Deployment of a basic Azure architecture, done first manually (portal/CLI, AZ-104 mindset) then reproduced in Terraform (Infrastructure as Code) — in the same repo, to demonstrate both skill sets.
┌─────────────────────────────┐
│ Resource Group │
│ │
│ ┌────────────────────────┐ │
│ │ VNet 10.0.0.0/16 │ │
│ │ ┌──────────────────┐ │ │
│ │ │ Subnet 10.0.1.0/24│ │ │
│ │ │ │ │ │
│ │ │ ┌────────────┐ │ │ │
Internet ───────┼──┼──┼──▶│ Linux VM │ │ │ │
(SSH:22, │ │ │ │ (Ubuntu │ │ │ │
HTTP:80) │ │ │ │ 22.04) │ │ │ │
│ │ │ └────────────┘ │ │ │
│ │ │ ▲ │ │ │
│ │ │ NSG │ │ │
│ │ │ (SSH + HTTP rules) │ │ │
│ │ └──────────────────┘ │ │
│ └────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ Storage Account │ │
│ └────────────────────────┘ │
└─────────────────────────────┘
azure-iac-lab/
├── MANUAL_SETUP.md # Documented manual deployment (portal/CLI)
├── main.tf # Azure resources (Terraform)
├── variables.tf # Configurable variables
├── outputs.tf # Outputs (public IP, names, etc.)
├── terraform.tfvars.example # Example values to copy into terraform.tfvars
├── .gitignore # Excludes tfstate, real tfvars, SSH key
└── .github/workflows/terraform.yml # CI: terraform plan on every PR
- An Azure account (free tier or Azure for Students is sufficient)
- Terraform ≥ 1.5:
terraform -v - Azure CLI:
az login
See MANUAL_SETUP.md for the full detail (real commands, errors encountered, time spent).
# 1. Azure authentication
az login
# 2. Copy and adapt variables
cp terraform.tfvars.example terraform.tfvars
# → edit allowed_ssh_source with your real public IP!
# 3. Initialize
terraform init
# 4. Preview changes
terraform plan
# 5. Apply
terraform apply
# 6. Retrieve the VM's public IP
terraform output vm_public_ip
# 7. Connect via SSH
terraform output -raw ssh_connection_command
# 8. Destroy the infrastructure once done (avoids unnecessary costs)
terraform destroy| Criterion | Manual (portal/CLI) | Terraform |
|---|---|---|
| Deployment time | ~1h (including region + SKU capacity debugging) | ~1min30 (a single terraform apply command) |
| Teardown time | A few seconds (az group delete --no-wait, asynchronous, no visible confirmation) |
~2 min (terraform destroy, sequential, with confirmation for each resource) |
| Number of commands | ~10 distinct commands to type and chain | 1 command (terraform apply) |
| Errors encountered | Region policy (RequestDisallowedByAzure) + SKU capacity unavailable (SkuNotAvailable), debugged manually one by one |
0 errors — the correct configuration (region + VM size) was directly baked into variables.tf thanks to lessons learned during the manual run |
| Reproducibility | Low — depends on the operator's memory and rigor | Total — the same result is guaranteed on every run |
| Infrastructure documentation | Manual, written separately (MANUAL_SETUP.md) |
The code (main.tf) is the documentation |
| Cleanup / teardown | Manual, dependency order must be handled by hand | terraform destroy automatically handles the dependency/deletion order |
| Scaling (e.g. 10 VMs) | Repetitive, error-prone with each repetition | A single count or for_each variable, no extra effort |
The most notable finding: both errors encountered manually (Azure for Students region policy restricted to swedencentral, and SkuNotAvailable capacity errors across several VM sizes before finding Standard_B2ats_v2) would have been just as blocking with Terraform — but once understood and fixed once in variables.tf, they never resurface. The manual approach requires remembering the fix on every future attempt; Terraform encodes it permanently.
- The SSH rule defaults to
*in the examples: restrict it to your own IP in practice (terraform.tfvars). - The SSH private key is generated automatically by Terraform (
*.pem) and excluded from the repo via.gitignore. - Never commit
terraform.tfvarsif it contains sensitive values, nor*.tfstatefiles (they may contain plaintext secrets).
Every Pull Request automatically triggers terraform fmt, terraform validate, and terraform plan,
with the result posted as a comment on the PR (see .github/workflows/terraform.yml).
GitHub Actions secrets to configure (Settings > Secrets and variables > Actions):
AZURE_CREDENTIALS, ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_SUBSCRIPTION_ID, ARM_TENANT_ID
(created via a Service Principal: az ad sp create-for-rbac --sdk-auth).
Hiba bensaid — Data Science & Computer Science Engineering Student | Cloud & Automation Enthusiast