feat: Terraform IaC for per-client Azure provisioning - #2
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Terraform-based Azure IaC implementation under infra/azure/ to provision per-client infrastructure (VM, networking, managed identity, and a per-client backup blob container), and updates infra documentation to reflect the Terraform approach and the provider-isolation contract.
Changes:
- Introduces root Terraform configuration (
providers.tf,variables.tf,main.tf,outputs.tf) for per-client Azure provisioning. - Adds two Terraform modules:
modules/vm(VM + networking + managed identity + role assignment) andmodules/backup-container(per-client blob container in shared storage). - Updates
infra/README.mdand adds design/implementation plan docs describing the new approach.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/azure/providers.tf | Pins Terraform/provider requirements and configures the AzureRM provider. |
| infra/azure/variables.tf | Defines the input contract for per-client provisioning. |
| infra/azure/main.tf | Composes the VM and backup-container modules. |
| infra/azure/outputs.tf | Exposes public IP, RG name, and backup container name as the output contract. |
| infra/azure/modules/vm/main.tf | Implements resource group, identity + role assignment, VNet/subnet, NSG, NIC, and Linux VM. |
| infra/azure/modules/vm/variables.tf | Module inputs for VM provisioning. |
| infra/azure/modules/vm/outputs.tf | Module outputs for public IP and resource group name. |
| infra/azure/modules/backup-container/main.tf | Creates a per-client blob container in a shared storage account. |
| infra/azure/modules/backup-container/variables.tf | Module inputs for backup container creation. |
| infra/azure/modules/backup-container/outputs.tf | Module output for the container name. |
| infra/azure/clients/edmonton.tfvars | Example per-client tfvars file (non-secret values + SSH public key placeholder). |
| infra/azure/.gitignore | Ignores local Terraform state and plan artifacts for this directory. |
| infra/README.md | Updates infra documentation from placeholder Bicep/ARM to Terraform usage instructions. |
| docs/plans/2026-02-22-infra.md | Adds a step-by-step implementation plan for the Terraform infra work. |
| docs/plans/2026-02-22-infra-design.md | Adds design rationale and the provider-isolation contract description. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Note: `.terraform.lock.hcl` is intentionally NOT ignored — it pins provider versions and should be committed (like a lockfile). | ||
|
|
||
| **Step 2: Create providers.tf** | ||
|
|
||
| ```hcl | ||
| # infra/azure/providers.tf | ||
| terraform { | ||
| required_version = ">= 1.9" | ||
| required_providers { | ||
| azurerm = { | ||
| source = "hashicorp/azurerm" | ||
| version = "~> 4.0" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| provider "azurerm" { | ||
| features {} | ||
| # subscription_id is read from ARM_SUBSCRIPTION_ID environment variable. | ||
| # Set it before running terraform: export ARM_SUBSCRIPTION_ID="<your-sub-id>" | ||
| } | ||
| ``` | ||
|
|
||
| **Step 3: Run terraform init** | ||
|
|
||
| ```bash | ||
| cd infra/azure | ||
| terraform init | ||
| ``` | ||
|
|
||
| Expected: "Terraform has been successfully initialized!" and `.terraform.lock.hcl` is created. | ||
|
|
||
| **Step 4: Commit** | ||
|
|
||
| ```bash | ||
| git add infra/azure/.gitignore infra/azure/providers.tf infra/azure/.terraform.lock.hcl | ||
| git commit -m "feat: bootstrap infra/azure with terraform provider config" | ||
| ``` |
There was a problem hiding this comment.
This plan states that .terraform.lock.hcl "should be committed", and the sample commit command includes it, but the PR doesn't add the lockfile. Either commit infra/azure/.terraform.lock.hcl (recommended to pin provider versions) or update the plan to avoid implying it’s present/committed.
| security_rule { | ||
| name = "SSH" | ||
| priority = 100 | ||
| direction = "Inbound" | ||
| access = "Allow" | ||
| protocol = "Tcp" | ||
| source_port_range = "*" | ||
| destination_port_range = "22" | ||
| source_address_prefix = "*" | ||
| destination_address_prefix = "*" | ||
| } |
There was a problem hiding this comment.
The SSH NSG rule currently allows inbound TCP/22 from any source (source_address_prefix = "*"). This exposes the VM SSH port to the internet; consider restricting this to an allowlisted CIDR (e.g., an input variable like ssh_source_cidr) and/or documenting why unrestricted SSH is acceptable.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
infra/azure/Terraform configuration to provision a per-client VM, networking, managed identity, and backup blob container on Azureinfra/azure/) establishes an explicit isolation seam — a futureinfra/aws/would accept the same inputs and produce the same outputsinfra/README.mdwith the actual Terraform approach and usage instructionsWhat gets provisioned
Per
terraform apply -var-file=clients/edmonton.tfvars:tiller-{client_name}Storage Blob Data Contributoron the shared backup account — enablesbackup.sh --auth-mode loginwithout stored credentials{client_name}-backupsin pre-existing sharedtillerbackupsstorage accountState is local (
terraform.tfstate, gitignored). The.tfvarsfiles inclients/are committed — they contain no secrets (SSH public key only).Test Plan
terraform init && terraform validateininfra/azure/— requires Terraform >= 1.9 installedterraform plan -var-file=clients/edmonton.tfvars— requiresARM_SUBSCRIPTION_IDset andaz loginauthenticated