TL;DR: A Node.js API, Terraform, AKS, ACR, GitHub Actions, Trivy, SonarQube, OWASP ZAP, Helm, and zero secrets in CI. All on the Azure free tier.

I wanted to build something I could actually explain in an interview when someone asks "Walk me through your CI/CD pipeline and how you handle security." So I built one from scratch.

The Pipeline

PR → Tests → SAST → SCA → IaC Scan → Build → Container Scan
→ Push ACR → Deploy Staging → DAST → Approval → Production

Every single gate is blocking. If Trivy finds a CRITICAL CVE in your image, the pipeline fails. No overrides, no "I'll fix it later."

What I Used

Concern Tool
IaC Terraform (azurerm)
CI/CD GitHub Actions
Container Docker (distroless, non-root)
Orchestration AKS + Helm
Registry ACR (Basic)
SAST SonarQube
SCA + Container + IaC Trivy
DAST OWASP ZAP
Secrets Gitleaks
Auth Azure Workload Identity Federation

The Part I'm Most Proud Of: No Secrets in CI

Most tutorials show you storing an Azure service principal JSON in GitHub secrets. That's a long-lived credential sitting in an encrypted blob. I didn't want that.

Instead:

  1. I created a user-assigned managed identity in Azure
  2. Added a federated credential that trusts GitHub Actions' OIDC token
  3. Granted that identity Contributor on my resource groups
  4. In my workflow, I just use azure/login@v2 with three repo variables (not secrets):
 yaml - uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} That's it. No client secret. No certificate. GitHub mints a short-lived OIDC token, Azure validates it against the federated credential, and you get a token that expires in 1 hour. If someone leaks your repo, they get nothing useful. **Terraform Structure** terraform/ ├── modules/ │ ├── aks-cluster/ # AKS + node pools + Workload Identity │ ├── acr/ # Container Registry │ ├── vnet/ # VNet + subnets │ └── log-analytics/ # Log Analytics workspace └── environments/ ├── staging/ # 1 B1s node, Basic ACR └── production/ # 1 B1s node (HPA to 3), Basic ACR Remote state lives in Azure Storage (blob backend). Each environment has its own key. **Helm Chart Highlights** `readOnlyRootFilesystem: true runAsNonRoot: true capabilities.drop: [ALL] NetworkPolicy (ingress + egress) PodDisruptionBudget HPA on production` **Free Tier Reality Check** | Resource | Cost | |----------|------| | AKS (Free SKU) × 2 | $0 | | B1s nodes (750 hrs/mo) | $0 | | ACR Basic × 2 | $0 (12 mo) | | VNet, NSG, Storage | $0 | | Load Balancer | ~$18/mo | What I'd Do Differently I'd add Admission Controllers (Kyverno or OPA Gatekeeper) to enforce image signatures at the cluster level I'd use ACR Task for automated scanning on push (defense in depth — Trivy in CI is the gate, ACR Task is the backstop) I'd add chaos engineering (LitmusChaos) to validate the rollback path The Point This isn't a "production system." It's a learning project that happens to follow production patterns. But the patterns are real: shift-left security, zero-trust auth, environment promotion, immutable deployments, and automated rollback. What I'd Do Differently I'd add Admission Controllers (Kyverno or OPA Gatekeeper) to enforce image signatures at the cluster level I'd use ACR Task for automated scanning on push (defense in depth — Trivy in CI is the gate, ACR Task is the backstop) I'd add chaos engineering (LitmusChaos) to validate the rollback path The Point This isn't a "production system." It's a learning project that happens to follow production patterns. But the patterns are real: shift-left security, zero-trust auth, environment promotion, immutable deployments, and automated rollback. Repo Link: [](https://github.com/Techgirli/Azure-startup-api)