Infrastructure as Code
Azure k3s platform
A small production-style Kubernetes platform on Azure, built only from code: Terraform creates the cloud parts, Ansible hardens the servers and installs k3s, and a pipeline applies every change after review, without stored secrets.
At a glance
- 3
- VMs, one command
- 0
- secrets in the pipeline (OIDC)
- 2
- reviews before a change: plan and approval
- 1
- command to destroy it all
The goal
Servers made by hand drift apart, nobody knows who changed what, and a lost VM takes a day to rebuild. The goal: a platform where the whole environment is code, every change is reviewed as a plan before it runs, the servers are hardened the same way every time, and the cost stays under control.
What it shows
- Infrastructure as Code
- Terraform (azurerm 4) with two modules (network, node) and remote state in Azure Storage with Entra ID login.
- Networking
- VNet and NSG with deny by default, NAT gateway for outbound traffic, a Standard Load Balancer for 80/443, admin access only from your IP.
- Identity and secrets
- GitHub OIDC (no client secret), managed identities on the VMs, the k3s join token in Key Vault.
- Hardening
- SSH, sysctl, auditd, fail2ban, automatic updates; k3s secrets encryption, Pod Security, audit log and an encrypted pod network.
- Observability
- Prometheus, Alertmanager, Grafana, Loki, Fluent Bit with own alert rules, and Azure Monitor for the system logs.
- Cost control
- A budget with a forecast alert, small VM sizes, a log cap and one command to delete everything.
Architecture
The Azure environment. Only the load balancer is public for websites; SSH and the Kubernetes API answer only to the admin IP.
Step by step
Pull request
terraform fmt and validate, tflint and a Trivy IaC scan. Then the pipeline logs in to Azure with OIDC and writes the plan as a comment on the pull request.
Review
A human reads the plan: what will be created, changed or destroyed. Nothing has changed in Azure yet.
Apply
After the merge, the apply waits for an approval of the "production" environment. Then Terraform builds exactly the reviewed plan.
Configure
Ansible hardens the three Ubuntu servers, installs k3s with encryption and audit log, and adds the platform charts.
Every night
A drift check runs terraform plan. If Azure differs from the code, it opens an issue.
Proof from a real run
Run on the main branch on 2026-10-27, from GitHub Actions. Time per job:
- fmt, validate, tflint, Trivy 23s
- ansible-lint (production profile) 22s
Open this run (opens in a new tab)
Security controls
| Control | What it stops |
|---|---|
| NSG deny by default + admin_cidrs | SSH and the Kubernetes API open to the internet. |
| No public IP on the agents | Direct attacks on the worker nodes. |
| GitHub OIDC | A long-lived cloud secret that can leak from CI. |
| Key Vault + managed identity | The cluster join token in files or in Git. |
| Plan review + approval | A surprise delete in production. |
| tflint + Trivy IaC scan | Insecure or wrong settings before they reach Azure. |
| Nightly drift check | Manual changes that nobody knows about. |
| Budget alert | A forgotten lab that costs money every day. |
Try it yourself
az login && make bootstrap # once: state storage + GitHub OIDC app
cp terraform/terraform.tfvars.example terraform/terraform.tfvars # your IP and SSH key
make init plan apply # VNet, NSG, NAT, load balancer, 3 VMs, Key Vault, monitoring
make configure # Ansible: hardening, k3s, platform charts
make kubeconfig && KUBECONFIG=./kubeconfig kubectl get nodes
make destroy # after the demo: everything is goneWhat I learned
- A clean plan in the pull request is the best review tool: people read what will change, not the code.
- Secure defaults belong in the code: the web ports stay closed until you open them on purpose.
- A platform you can destroy and rebuild in minutes is cheaper and safer than one you are afraid to touch.