DevSecOps projects

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.

  • Terraform
  • Azure
  • Ansible
  • k3s
  • Helm
  • Prometheus
  • Grafana
  • Loki
  • Fluent Bit
  • cert-manager
  • Key Vault
  • GitHub Actions
  • OIDC

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.

The Azure k3s platform: network, nodes, cluster, Key Vault, monitoring and the pipelineAzure — resource group (Terraform)VNet 10.40.0.0/16 · NSG: deny by defaultAdmin laptopadmin_cidrs onlyGitHub ActionsOIDC loginno stored secretUserswebsitesInternetupdates, imagesk3s servercontrol plane · tainted10.40.1.10 · public IPk3s agent 110.40.1.20no public IPk3s agent 210.40.1.21no public IPIn the cluster· Traefik ingress· cert-manager· Prometheus· Alertmanager· Grafana· Loki + Fluent Bit· WireGuard pod network· your appsKey Vaultk3s join token (RBAC)Log Analyticssyslog via Azure Monitor AgentStandard Load Balancer80 / 443 → agentsNAT gatewayall outbound trafficBudget alertforecast at 80 %Storage account: Terraform stateEntra ID login · versioningSSH 22 · API 644380/443ingressoutidentitylogsplan / apply

Step by step

  1. 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.

  2. Review

    A human reads the plan: what will be created, changed or destroyed. Nothing has changed in Azure yet.

  3. Apply

    After the merge, the apply waits for an approval of the "production" environment. Then Terraform builds exactly the reviewed plan.

  4. Configure

    Ansible hardens the three Ubuntu servers, installs k3s with encryption and audit log, and adds the platform charts.

  5. 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

ControlWhat it stops
NSG deny by default + admin_cidrsSSH and the Kubernetes API open to the internet.
No public IP on the agentsDirect attacks on the worker nodes.
GitHub OIDCA long-lived cloud secret that can leak from CI.
Key Vault + managed identityThe cluster join token in files or in Git.
Plan review + approvalA surprise delete in production.
tflint + Trivy IaC scanInsecure or wrong settings before they reach Azure.
Nightly drift checkManual changes that nobody knows about.
Budget alertA 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 gone

What 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.
Next case study Secure delivery pipeline