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.
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.
| Piece | Our choice | Why |
|---|---|---|
| Resource group | contoso-infra-code in East US | A separate home for disposable resources. |
| VNet / subnet | 10.43.0.0/16 / 10.43.1.0/24 | No existing network dependency; implicit default outbound access disabled. |
| Compute | Standard_B1s, Ubuntu 24.04 x64 | Small burstable machine; availability and trial quotas vary. |
| Network interface | One NIC + Standard static IPv4 | The explicitly attached public IP provides internet connectivity. It is billable separately. |
| Disk | Standard SSD LRS managed OS disk | Provider-managed encryption at rest; size inherited from the image. |
| Inbound | 22 and 80 from your /32; deny everything else | The 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.
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
- Finish the Azure account/credit checks, enable MFA and confirm the intended learning subscription. VM access and quota are not guaranteed by trial credit.
- 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.
- 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.
- 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.
- 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
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.
wsl --install -d UbuntuRestart 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.
Use an Ubuntu terminal. The following local package commands target Ubuntu; other Linux distributions can administer this cloud VM with equivalent tools.
Install Azure CLI and Terraform from official instructions inside Ubuntu/WSL. Use interactive user login—no client secret is needed for this lab.
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 versionReplace the subscription placeholder. Stop if the selected account is wrong. If registration is denied, ask the administrator; do not expand permissions yourself.
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"
fiThe .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.
- Create resource group
contoso-infra-manualin East US, then a virtual networkcontoso-infra-manual-vnetwith address space10.43.0.0/16and one subnet10.43.1.0/24. Disable default outbound access for that subnet; no NAT gateway is needed. - 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.
- 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. - Use SSH public-key authentication, username
contoso, and your existing public-key file shown byrealpath "$LAB_SSH_KEY.pub"key. Do not enable password authentication. - Choose a Standard SSD LRS managed OS disk with its image-supported size, no data disks. Do not shrink it below the image minimum.
- 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.
- 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.
- 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.
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.
for key in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$key"; doneRun Command requires the Azure VM agent and appropriate permission; ask the administrator if unavailable. Do not disable SSH verification as a shortcut.
ssh -i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" contoso@YOUR-SERVER-IPsudo 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/
exitIf 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.
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.
subscription_id = "YOUR-SUBSCRIPTION-ID"
allowed_ipv4_cidr = "203.0.113.10/32"
public_key_path = "/REPLACE/WITH/YOUR/ABSOLUTE/PUBLIC-KEY-PATH.pub"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 outputA 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.
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.
7. Debug one layer at a time
| Symptom | Check |
|---|---|
| Size unavailable / quota failure | Subscription quotas, region, image architecture and permitted size. Do not upgrade billing or deploy a large fallback blindly. |
| SSH timeout | Running VM, correct public IP/NIC, current /32, effective NSG rules, local VPN/firewall. |
| Public key rejected | contoso username and matching private key. Opening another port does not repair credentials. |
| Package downloads fail | Attached public IP, outbound NSG rules and DNS. The disabled implicit outbound path is not a free NAT service. |
| HTTP fails but SSH works | Local curl, Nginx status and listening port, then the port 80 NSG rule. |
| Costs after shutdown | Stopped versus deallocated, retained managed disks, static public IPv4, backups or other added services. |
| Host key changed | Confirm a real replacement through Azure, verify its new fingerprint, then remove only the obsolete local entry. Never turn off checking. |
sudo nginx -t
systemctl status nginx --no-pager
sudo journalctl -u nginx -n 30 --no-pager
sudo ss -lntp
df -h
free -hUse 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
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
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 logoutUse 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.