Azure · Real infrastructure

A Linux server.
The Azure way.

Build the network, firewall, address and disk around a real Ubuntu computer. Then rebuild with Terraform and configure the machine with Ansible.

Ubuntu 24.04Windows + Linux laptopsYour IP onlyConsole + copyable code

These are sample names, not our infrastructure. Before copying commands, read Customize the examples: paths, keys, IPs and names. Substitute your own authorized account and resources. Existing lab? Retain its original resource names, usernames and state; renamed samples can cause replacement. See the existing-lab warning before applying updated recipes.

One computer, with its own neighborhood

A resource group is the labeled box holding this lab. A virtual network is its neighborhood. The network security group (NSG) checks which doors your laptop may use. An SSH key proves you can enter the Ubuntu account.

Your laptopPublic IPv4 /32
Public IP + subnet NSGSSH 22 · demo HTTP 80
Ubuntu VMNginx and an OS disk
PieceOur choiceWhy
Resource groupcontoso-infra-code in East USA separate home for disposable resources.
VNet / subnet10.43.0.0/16 / 10.43.1.0/24No existing network dependency; implicit default outbound access disabled.
ComputeStandard_B1s, Ubuntu 24.04 x64Small burstable machine; availability and trial quotas vary.
Network interfaceOne NIC + Standard static IPv4The explicitly attached public IP provides internet connectivity. It is billable separately.
DiskStandard SSD LRS managed OS diskProvider-managed encryption at rest; size inherited from the image.
Inbound22 and 80 from your /32; deny everything elseThe added deny rule also overrides Azure's default VNet/load-balancer inbound allows for this isolated lab.

Azure provides system routes; we do not create AWS-style internet gateway or route-table resources here. Disabling default outbound access does not make this VM private: its NIC has an explicit public IP. NSG default outbound rules remain permissive for package downloads.

Short-lived lab, not production hosting. SSH is encrypted; the demo uses unencrypted HTTP and no sensitive content. Your source IP restriction prevents general public access, but it is not a substitute for TLS, OS updates or application security. Do not bypass browser HTTPS/security warnings; use terminal curl if needed.

No managed identity is attached: this server does not need Azure API permissions. There is no NAT gateway, load balancer, Bastion, backup vault or extra disk.

Microsoft: explicit outbound connectivityNSG rules and defaultsCanonical image identifiers

1. Know the subscription and the bill

  1. Finish the Azure account/credit checks, enable MFA and confirm the intended learning subscription. VM access and quota are not guaranteed by trial credit.
  2. An administrator should authorize Contributor on an isolated learning subscription for this simple recipe, which creates its own resource group. This is broad management access: do not copy that scope into production. A pre-created group and narrower scope are a later refinement.
  3. Confirm Microsoft.Compute and Microsoft.Network are registered in the subscription. Registration needs appropriate subscription-level permission; ask an administrator if your role is narrower.
  4. Set a monthly $5 budget with spend/forecast alerts, check trial credit separately, and keep a personal cleanup timer. A budget does not stop a VM.
  5. Price 4 running hours of Linux Standard_B1s in East US, the image's OS disk size/tier for its full lifetime, one Standard public IPv4 for its full allocation time and a little outgoing data. Do not run manual and code copies together unnecessarily.

Disk billing depends on the provisioned capacity tier, not only how much content you write. Check the actual disk size after creation and its matching tier in the calculator. The Ubuntu image commonly starts with about 30 GiB; do not assume the 8 GiB AWS disk applies here.

Stopped is not always deallocated. Guest OS shutdown can leave compute allocated and billable. Deallocation removes compute allocation, but managed disks and this static public IP remain. Delete the lab when done.

Azure calculatorPublic IP pricingManaged disk tiersVM states and billingBudget alerts

Ready: correct subscription, authorized scope, registered providers, budget and price estimate. If policy blocks public VMs or the selected image/size, use an authorized sandbox rather than weakening it.

2. Prepare the laptop

Use Ubuntu in WSL for all later commands. Your laptop remains Windows; your cloud server is Ubuntu. Keep Azure CLI, Terraform, SSH, Ansible and their credentials in that one environment.

Administrator PowerShell · install WSL
wsl --install -d Ubuntu

Restart if asked, finish Ubuntu setup, and open its terminal. Store the lab beneath the Ubuntu home directory, not a shared Windows folder. See WSL setup.

Install Azure CLI and Terraform from official instructions inside Ubuntu/WSL. Use interactive user login—no client secret is needed for this lab.

Ubuntu / WSL · select the subscription
az login
az account list --output table
az account set --subscription YOUR-SUBSCRIPTION-ID
az account show --query '{name:name,id:id}' --output table
az provider register --namespace Microsoft.Compute --wait
az provider register --namespace Microsoft.Network --wait
terraform version

Replace the subscription placeholder. Stop if the selected account is wrong. If registration is denied, ask the administrator; do not expand permissions yourself.

Ubuntu / WSL · create a separate key and project
sudo apt update
sudo apt install -y openssh-client python3-venv
mkdir -p ~/contoso-azure-infra
cd ~/contoso-azure-infra
# Edit this path if you want a different dedicated key location.
LAB_SSH_KEY="$HOME/.ssh/contoso_azure"
# Stop if either file exists: never overwrite a key.
if [ -e "$LAB_SSH_KEY" ] || [ -e "$LAB_SSH_KEY.pub" ]; then
  printf '%s\n' 'Key already exists. Choose a new path, or reuse it only if authorized.'
else
  # Choose a passphrase. Continue only if creation and unlocking succeed.
  mkdir -p "$(dirname "$LAB_SSH_KEY")" &&
  ssh-keygen -t rsa -b 4096 -f "$LAB_SSH_KEY" -C contoso &&
  eval "$(ssh-agent -s)" &&
  ssh-add "$LAB_SSH_KEY" &&
  cat "$LAB_SSH_KEY.pub" &&
  realpath "$LAB_SSH_KEY.pub"
fi

The .pub file goes to Azure. Keep the private file and its passphrase outside the project and out of source control. Find the current public IPv4 used by your home/VPN connection and append /32. This is not your laptop's private 192.168.x.x address.

Your key path is your choice. $HOME expands to the home directory of the Ubuntu/WSL user running these commands; no username is hard-coded. Keep using the same LAB_SSH_KEY in this terminal. In a new terminal, set it again to your actual private-key path and unlock the key. Terraform needs the matching .pub file: replace its path placeholder with the output of realpath "$LAB_SSH_KEY.pub". Ansible needs the private file: edit both the inventory IP and its key path. A literal $LAB_SSH_KEY is not expanded inside HCL or an INI inventory.

3. Build once in the Azure portal

Use separate names ending in manual. Do not deploy anything into a resource group holding existing work.

  1. Create resource group contoso-infra-manual in East US, then a virtual network contoso-infra-manual-vnet with address space 10.43.0.0/16 and one subnet 10.43.1.0/24. Disable default outbound access for that subnet; no NAT gateway is needed.
  2. Create an NSG in this group. Add inbound TCP 22 from your IPv4 /32 at priority 100 and TCP 80 from the same source at 110. Add a deny-all inbound rule at 4096. Associate this NSG with the subnet. Keep default outbound rules.
  3. Create a VM named contoso-infra-manual. Select the same group, East US, Canonical Ubuntu Server 24.04 LTS x64 Gen2 and Standard_B1s. If unavailable, stop and compare another size's price and image compatibility.
  4. Use SSH public-key authentication, username contoso, and your existing public-key file shown by realpath "$LAB_SSH_KEY.pub" key. Do not enable password authentication.
  5. Choose a Standard SSD LRS managed OS disk with its image-supported size, no data disks. Do not shrink it below the image minimum.
  6. Select the existing VNet/subnet and a new Standard static public IPv4 on the NIC. Choose no NIC-level NSG if using the subnet NSG; do not add an automatic “SSH from Anywhere” rule or check a box to allow unrestricted HTTP.
  7. Leave managed identity off, boot diagnostics off if not needed, and paid agents/backup/Bastion/load-balancing features out. Review the entire summary and price before creation. Do not bypass mandatory subscription security policies; choose a permitted lab design instead.
  8. Wait until running. Check the VM's effective security rules: both learner allows should be followed by the custom deny. Record the public IP, VM, NIC, disk and group names.
Prove it: one VM, one NIC/public IP, one OS disk and one restricted subnet. You can explain what gives this machine internet access.

4. Verify the server before entering

In the portal, open the VM → Run command → RunShellScript. Run this read-only command through your authenticated Azure session. Compare the fingerprint for the key type SSH presents—not your personal login key's fingerprint.

Azure Run Command · server host fingerprints
for key in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$key"; done

Run Command requires the Azure VM agent and appropriate permission; ask the administrator if unavailable. Do not disable SSH verification as a shortcut.

Ubuntu / WSL · connect after matching the fingerprint
ssh -i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" contoso@YOUR-SERVER-IP
Inside the manual server · inspect and install
sudo cloud-init status --wait
cat /etc/os-release
ip -brief address
free -h
df -h
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
curl -I http://127.0.0.1/
exit

If cloud-init reports errors, investigate first. From the permitted laptop open http://YOUR-SERVER-IP/ or run curl -I http://YOUR-SERVER-IP/. Expect Nginx's default page. HTTP is only a demo; no sensitive information belongs in it.

Microsoft: SSH and host verificationRun Command permissions

5. Rebuild with Terraform

Clean up the manual group first to avoid duplicate costs. This recipe creates a separate group named contoso-infra-code; it does not import your manual VM. If that name already belongs to other work, stop and choose consistently different names in the recipe.

Save these files in ~/contoso-azure-infra. Replace all placeholders in the variables. The example IP is documentation-only and will not allow your connection.

terraform.tfvars
subscription_id  = "YOUR-SUBSCRIPTION-ID"
allowed_ipv4_cidr = "203.0.113.10/32"
public_key_path   = "/REPLACE/WITH/YOUR/ABSOLUTE/PUBLIC-KEY-PATH.pub"
Ubuntu / WSL · review and build
az account show --query '{name:name,id:id}' --output table
terraform init
terraform fmt
terraform validate
terraform plan -out=lab.tfplan
# Check the subscription, group, size, IP and every NSG rule.
terraform apply lab.tfplan
terraform output

A saved plan applies without another confirmation. Stop before apply if the plan contains unexpected services, replacements or open inbound access. Keep state and saved plans private; keep the provider lockfile for repeatable versions. Azure encrypts managed disks at rest by default; this lab does not create customer-managed encryption keys.

The recipe uses the current Ubuntu image version at deployment. Pin and intentionally update approved image versions in real environments. A Terraform validation check only checks configuration—it does not prove regional capacity, permissions, policy or runtime behavior.

Get the new public IP from outputs. Verify this new VM's host fingerprints through Run Command, SSH into it, wait for cloud-init, then exit. Nginx is intentionally absent until Ansible runs.

Terraform Linux VMSubnet outbound settingManaged disk encryption

6. Configure the same Ubuntu machine

The Ubuntu playbook is shared with the AWS lab: install Nginx, render an HTML template, enable the service and verify a local HTTP response. Only the inventory's username, IP and key change. Download all three files into the same local folder.

Replace YOUR-SERVER-IP and the absolute private-key path in the inventory. The SSH agent must hold your unlocked Azure key, and the server must already have a verified known-host entry.

Ubuntu / WSL · configure then change a sentence
python3 -m venv ~/.venvs/contoso-azure-infra
source ~/.venvs/contoso-azure-infra/bin/activate
python -m pip install ansible-core
ansible-playbook -i inventory.ini server.yml --syntax-check
ansible lab -i inventory.ini -m ansible.builtin.ping
ansible-playbook -i inventory.ini server.yml
# Now edit page_message in server.yml.
ansible-playbook -i inventory.ini server.yml --check --diff
ansible-playbook -i inventory.ini server.yml
curl http://YOUR-SERVER-IP/

Ansible ping tests SSH/Python, not ICMP. Use check mode after the first real installation; simulation cannot prove a successful deployment. Run again: packages, the template and service should normally stay unchanged, though cache refreshes may report changes. Terraform should not see an infrastructure change from a page edit.

Prove it: the edited page is visible from your allowed laptop, a repeat playbook is stable, and you can explain why Terraform did not install Nginx.

Ansible packagesCheck mode

7. Debug one layer at a time

SymptomCheck
Size unavailable / quota failureSubscription quotas, region, image architecture and permitted size. Do not upgrade billing or deploy a large fallback blindly.
SSH timeoutRunning VM, correct public IP/NIC, current /32, effective NSG rules, local VPN/firewall.
Public key rejectedcontoso username and matching private key. Opening another port does not repair credentials.
Package downloads failAttached public IP, outbound NSG rules and DNS. The disabled implicit outbound path is not a free NAT service.
HTTP fails but SSH worksLocal curl, Nginx status and listening port, then the port 80 NSG rule.
Costs after shutdownStopped versus deallocated, retained managed disks, static public IPv4, backups or other added services.
Host key changedConfirm a real replacement through Azure, verify its new fingerprint, then remove only the obsolete local entry. Never turn off checking.
Inside Ubuntu · inspect health
sudo nginx -t
systemctl status nginx --no-pager
sudo journalctl -u nginx -n 30 --no-pager
sudo ss -lntp
df -h
free -h

Use Azure VM metrics for platform checks. Guest memory and filesystem fullness need OS checks or a configured monitoring agent; logs/agents can add costs. If taking a break, deallocate through the portal and confirm the state, but understand that this lab's disk and static IP still remain.

8. Remove every lab resource

Destructive. Save wanted files locally first. A resource-group deletion removes everything inside it. Confirm it contains only this disposable lab—not your website storage or other work.

Manual copy

Inspect contoso-infra-manual → Resources. Check every item, then delete that exact group through the portal and wait for completion. Verify the VM, OS disk, public IP, NIC, NSG and VNet are gone. VM deletion alone can leave disks and networking behind.

Code copy

Ubuntu / WSL · reviewed Terraform cleanup
az account show --query '{name:name,id:id}' --output table
terraform state list
terraform plan -destroy -out=cleanup.tfplan
# Check every resource and the group contents before applying deletions.
terraform apply cleanup.tfplan
terraform state list
az logout

Use the original Terraform folder/state and keep the public-key file available for planning. Do not add unrelated resources to a Terraform-owned lab group: deleting the group can delete those too. Confirm cleanup in the portal, then inspect Cost Management after reporting catches up. Earlier usage is still billable.

Finished: no manual/code VM, retained disk, static IP or unintended paid service remains. Local recipes remain available for a deliberate rebuild.

Continue with Google Cloud · Infrastructure foundations