I got an email from Medline, a $25 billion healthcare company, inviting me to a 75-minute Teams interview for a Cloud Engineer position. The job description was specific: Azure, AKS, Terraform, Rancher, Azure DevOps, Prometheus. Instead of spending those four days preparing slides and rehearsing answers, I decided to build the exact infrastructure they described.
This article walks through every architectural decision, every technical mistake I hit, and the complete stack I assembled.
| Feature | Terraform | OpenTofu || — — — — -| — — — — — -| — — — — — || License | BUSL | MPL 2.0 || State compatible | Yes | Yes || Cloud support | Yes | Yes |
Medline just went public on Nasdaq in December 2025, raising over $6 billion at a $37 billion valuation. They operate healthcare supply chains across Europe, with their German operations headquartered in Kleve. The Cloud Engineer role sits in their Infrastructure Operations team, responsible for maintaining and continuously improving their Azure and Kubernetes platforms.
The job description listed these requirements:
I wanted to walk into the interview and say: 'Here's a running platform. Let me show you.' Not slides. Not theory.
The platform has two planes: a management plane and an application plane.
Themanagement clustersits in France Central and runs all operational tooling: Rancher for multi-cluster management, ArgoCD for GitOps deployments, Prometheus and Grafana for centralized monitoring, NGINX Gateway Fabric for HTTPS ingress, and cert-manager for automatic Let's Encrypt TLS certificates.
Threeapplication clustershandle the workloads:
Environment Region Nodes VNet CIDR Subnets dev Norway East 1 10.0.0.0/16 1 staging Spain Central 2 10.1.0.0/16 2 prod Switzerland North 3 10.5.0.0/16 4 mgmt France Central 3 10.0.0.0/16 1
I deployed the Docker Voting App (a five-service polyglot microservices application) across all three environments to have real workloads running, not just empty clusters.
I built four reusable Terraform modules:
Each module is generic. The environment-specific configuration lives entirely in Terragrunt.
The core problem with workspaces is duplication. Yourdev/main.tfandprod/main.tfend up being near-identical files. Change one, you have to remember to change the other.
With Terragrunt, you define the module call once inroot.hcland override only what differs per environment:
Thedependencyblock handles execution ordering automatically. Terragrunt knows VNet must exist before AKS, and AKS must exist before ACR (for the managed identity). A singleterragrunt run-all applyin thelive/dev/directory provisions everything in the correct order.
The remote state configuration is also centralized inroot.hcl:
Thepath_relative_to_include()function generates a unique state key per module per environment:dev/aks/terraform.tfstate,prod/network/terraform.tfstate, and so on. No collisions, no manual state management.
One mistake I made early: I created a resource group for Terraform state and tried to later move the storage account inside the management cluster's resource group to clean up the Azure portal.
This creates a circular dependency. Terraform needs the state file to know about the resource group that contains the state file. If you destroy the management stack, you lose all state for every environment.
The correct pattern: keeptfstate-rgas a manually created resource group, intentionally outside Terraform's lifecycle. That resource group should never appear in any.tffile.
I organized the project into three separate repositories:
The separation matters because each repo has a different change lifecycle. Updating Terraform modules should not trigger Docker builds. Changing application code should not re-plan infrastructure. The gitops-config repo is the single source of truth for what runs in each cluster.
Each service has its own chart with three values files:
The templates are identical. Only the values differ:
ArgoCD sits on the management cluster and manages all three application clusters remotely. Each cluster was registered viaargocd cluster add, which installs a ServiceAccount on the target cluster.
Each environment has one root Application that watches a folder in the gitops-config repo:
Thedirectory.exclude: root-app.yamlis critical. Without it, the root app discovers itself and tries to create itself, triggering an infinite loop.
When I add a new YAML file to theargocd/dev/folder and push, ArgoCD discovers it and deploys the service. When I delete a file, ArgoCD cleans up. No manualkubectl applyat any point after the initial setup.
Dev and staging haveautomatedsync. Prod has noautomatedblock, which means all changes to production require explicit manual approval through the ArgoCD UI.
The vote service kept crashing with:
The fix:
Rancher installs in about 30 minutes: cert-manager first (a dependency), then Rancher via Helm. Importing the three application clusters is straightforward. Rancher generates akubectl applycommand that installs a lightweight agent on each downstream cluster. Within a minute, all three appear in the Rancher dashboard.
This is precisely what Medline uses for multi-cluster management. Having it running gives you a live demo during the interview.
The Kubernetes community retired the NGINX Ingress Controller in March 2026. It stopped receiving security patches and bug fixes. The Ingress API itself is not deprecated, but it is feature-frozen. All innovation is happening in Gateway API, which graduated to GA in 2024.
Gateway API separates responsibilities cleanly: infrastructure teams manage GatewayClass and Gateway resources, application teams manage HTTPRoutes. No annotation hacks, no copy-pasting nginx.ingress.kubernetes.io annotations.
I used NGINX Gateway Fabric with per-hostname HTTPS listeners:
cert-manager with the Gateway API solver issues Let's Encrypt certificates automatically. Getting this to work required enabling theExperimentalGatewayAPISupport=truefeature gate during cert-manager installation. Without it, cert-manager silently ignores Gateway API annotations.
Medline confirmed they use Azure DevOps as their primary CI/CD tool. I made it the primary pipeline in the project, with GitHub Actions as a secondary option.
The CI pipeline detects which service changed, builds only that service, scans with Trivy, pushes to ACR, then commits the new image tag to the gitops-config repo. ArgoCD picks up the change automatically.
The[skip ci]in the commit message prevents a CI loop when the pipeline pushes to the gitops-config repo.
New Azure DevOps organizations don't get free hosted parallel jobs. Microsoft requires you to request a free grant (1–2 business days) or set up a self-hosted agent. I went with self-hosted:
Then changevmImage: "ubuntu-latest"toname: Defaultin every job that uses a pool.
The monitoring strategy: full Prometheus and Grafana on the management cluster, lightweight Prometheus agents on each application cluster that remote-write metrics to the central instance.
On each application cluster:
Every metric gets labeled withcluster=dev|staging|prod. One Grafana dashboard with a cluster selector shows all four environments. To verify the agents are sending data, run this PromQL query in Grafana:
You should see four clusters in the result.
The architecture has three independent gates between a code change and production:
Layer 1: Trivy in CI.Blocks any image with HIGH or CRITICAL CVEs from reaching ACR. The pipeline fails before the image is ever pushed.
Layer 2: Promotion pipeline.A separate, manually triggered Azure DevOps pipeline reads the current image tags from the source environment's values files and writes them to the target environment. Dev to staging happens automatically after CI succeeds. Staging to prod requires a manual approval step inside Azure DevOps before the pipeline proceeds.
Layer 3: ArgoCD manual sync.Even after the values file is updated in Git, ArgoCD will not deploy to prod automatically. Someone must click Sync in the ArgoCD UI or runargocd app sync prod-vote. Two independent human approvals are required for any production deployment.
Network policiesfor pod-to-pod isolation. Default-deny per namespace with explicit allow-lists between services (vote can talk to redis, worker can talk to redis and postgres, result can talk to postgres, nothing else).
Azure Key Vaultintegration via the CSI Secrets Store Driver. More Azure-native than HashiCorp Vault for an organization already deep in the Microsoft ecosystem.
Lokifor centralized log aggregation alongside metrics. Promtail on each cluster, logs labeled byclusterandnamespace, flowing to a central Loki instance.
Operational runbooksas markdown files in the gitops-config repo: CrashLoopBackOff diagnosis, node memory pressure investigation, deployment rollback procedure. These tie directly into the 'root cause analysis' requirement in the job description.
Category Tools Cloud Azure (AKS, ACR, VNet, Blob Storage, Managed Identity) IaC Terraform, Terragrunt Orchestration Kubernetes, Docker, Helm GitOps ArgoCD (app-of-apps) Multi-cluster Rancher Gateway NGINX Gateway Fabric, Kubernetes Gateway API, cert-manager CI/CD Azure DevOps Pipelines Monitoring Prometheus, Grafana Security Trivy Application Python (Flask), Node.js, .NET, Redis, PostgreSQL
Building under a deadline forced pragmatic decisions. Port-forward instead of a fourth public IP. Self-hosted agent instead of waiting for Microsoft's free grant. Incremental phases instead of trying to build everything before testing anything.
The project also reinforced something I already believed: the best way to prepare for a technical interview is to build the thing they're hiring you to operate. Theory is forgettable. A live demo running on real infrastructure is not.
The platform is organized across three repos on Azure DevOps. The code, diagrams, and full architecture documentation are linked from my portfolio at ghaithsaidani.me.
Ghaith Saidani is a Cloud and DevOps Engineer based in Frankfurt, Germany. CKA-certified. Open to Cloud, DevOps, SRE, and Platform Engineering roles where English is the working language.
Connect:LinkedIn|GitHub|Portfolio
(0)Comments