What I Learned Building the Cloud Resume Challenge on Azure
I'm working toward a cloud and systems administration career, and I wanted a project that proved I can do more than pass certification exams. So I took on the Cloud Resume Challenge (Azure): built a resume website, add a live visitor counter, and do it the way a real team would, with infrastructure as code, automated pipelines, and good security habits.
Live site: https://stresumelvoc8r.z1.web.core.windows.net/
Code: frontend ยท backend
Below is what I built, what broke along the way, and how each lesson connects to the jobs I'm applying for.
What I built
- Frontend: an HTML/CSS resume hosted as a static website on Azure Storage.
- Visitor counter: a small JavaScript file calls an API every time the page loads.
- API: a Python Azure Function on the Flex Consumption plan (serverless, scales to zero).
- Database: Cosmos DB (NoSQL, serverless) stores one record: the visitor count.
- Infrastructure as code: everything in Azure is defined in Terraform.
- CI/CD: GitHub Actions pipelines test, plan, and deploy every change, and production changes wait for my approval.
The rule I set for myself: no passwords, keys, or secrets stored anywhere. Not in the code, not in GitHub, not in the database connection.
Lesson 1: Infrastructure as code beats clicking around the portal
I've built plenty of things by clicking through the Azure portal. Terraform changed how I think about it. My entire environment (storage, database, function, monitoring, permissions) lives in text files I can read, review, and rebuild.
The concept that clicked for me was Terraform state. Terraform keeps a record of what it has built. I stored that state remotely in an Azure Storage account with versioning and soft delete turned on, so a mistake can be rolled back.
On the job: any team managing more than a handful of resources needs repeatable builds. Being able to say "I can define and rebuild an environment from code" is the difference between an admin who maintains things and one who can scale them.
Lesson 2: Pipelines without stored secrets (OIDC)
The old way to let GitHub deploy to Azure is to create a service principal, copy its secret, and paste it into GitHub. That secret can leak, and it expires at the worst possible time.
Instead I used OIDC (workload identity federation). GitHub proves to Azure who it is with a short-lived token, and Azure trusts that token only if it comes from my repo, in a pull request or the production environment. No secret exists to steal.
What broke: my first deploy failed with AADSTS700213: No matching federated identity record. The error showed that GitHub's token now includes the numeric IDs of my account and repo, not just their names. That stops someone from deleting a repo and recreating one with the same name to hijack the trust. My Azure configuration only trusted the old name-only format. Once I added credentials matching the new format, it worked.
On the job: reading an authentication error carefully and comparing "what was sent" with "what was expected" is everyday identity troubleshooting, the same skill I use for Entra ID sign-in issues.
Lesson 3: Least privilege, for real
It's easy to give a pipeline Owner and move on. I didn't. The pipeline's identity is:
- Contributor on one resource group, not the whole subscription.
- Allowed to grant permissions only from a short approved list of data roles, enforced with an Azure ABAC condition. Without that limit, an identity that can assign roles could make itself Owner.
On the job: "who has access to what, and why" comes up in every audit and security review. I can now explain role scope, privilege escalation risk, and how to prevent it, not just in theory.
Lesson 4: Managed identities mean no database keys
The Function App connects to Cosmos DB with a managed identity: Azure gives the app its own identity, and I granted it a database role. Cosmos DB has key-based access disabled entirely, so even if someone found my code, there's no key in it to use.
On the job: removing stored credentials from apps is one of the most effective security improvements an admin can make, and managed identities are how you do it in Azure.
Lesson 5: MFA isn't just for the portal anymore
Partway through, every Azure CLI command started failing with AADSTS50076, an MFA requirement, and it never actually prompted me for MFA. The cause: on Windows, the CLI was quietly reusing a cached sign-in through the Windows account broker, and that cached sign-in had no MFA. Clearing the cached tokens and signing in with az login --use-device-code forced a proper MFA sign-in.
On the job: Microsoft now requires MFA for Azure management from the CLI and other tools, not just the portal. When scripts or users suddenly fail to authenticate, knowing about cached tokens and broker sign-ins saves hours.
Lesson 6: The small stuff breaks things
Some of my most useful lessons were the least glamorous:
-
Line endings: Windows saves files with different line endings than Linux. A bash script saved the Windows way fails with confusing errors. A
.gitattributesfile fixed it for good. -
Git Bash path conversion: Git Bash on Windows quietly rewrites anything starting with
/, which broke Azure resource IDs until I turned that behavior off. -
Ignored files don't reach the pipeline: my Terraform variables file worked on my PC but not in GitHub Actions, because my
.gitignore(correctly) excluded it. I passed the value as a GitHub variable instead.
On the job: most real outages aren't exotic. They're a setting, a path, or an assumption about which machine a file lives on. Patience and methodical checking matter more than knowing everything.
Lesson 7: Pull requests and approval gates
Every change goes through the same path:
- Open a pull request, and the pipeline runs a Terraform plan that shows exactly what would change.
- Merge, and the deploy pauses for my approval.
- Approve, and it deploys.
I also learned to wait for green checks before merging. I merged a failing change once, and the deploy failed in exactly the way the check had warned me it would.
On the job: this is change management. Reviewing a change before it runs, and having an approval step for production, is the same idea as a change advisory process, just automated.
Lesson 8: Writing code I can test
The counter logic is small, but I wrote it carefully:
- It uses a Cosmos DB atomic increment, so two visitors arriving at the same moment can't overwrite each other's count.
- The logic is separate from the web layer, so I could write pytest tests that use fake (mocked) database objects. The tests run on every pull request without touching Azure.
- Errors return a generic message to the browser, and the details go to Application Insights for me to investigate.
On the job: even as an admin, I write scripts that other people depend on. Testing them before they run in production, and logging errors somewhere useful, is a habit that prevents incidents.
How this applies to the jobs I'm applying for
| What I did in this project | Where it shows up at work |
|---|---|
| Defined infrastructure in Terraform | Building and rebuilding environments consistently; documenting what exists |
| OIDC and managed identities | Removing stored passwords and keys; securing automation accounts |
| Scoped roles and ABAC conditions | Access reviews, audits, least-privilege design in Entra ID and Azure |
| Troubleshot MFA and token errors | Sign-in and Conditional Access issues for users and service accounts |
| Pull requests and approval gates | Change management and peer review |
| CI/CD pipelines | Automating repetitive admin tasks safely |
| Monitoring with Application Insights | Finding the cause of failures quickly |
| Serverless and pay-per-use services | Keeping cloud costs low and predictable |
What's next
- Custom domain with HTTPS through Cloudflare.
- An end-to-end test that checks the live site and API after every deploy.
- A containerized app on Kubernetes, to build on what I learned here.
If you're working through the Cloud Resume Challenge yourself, my advice is: read the error message twice before searching it. Almost every problem I hit was explained right there in the error, once I slowed down enough to compare what was sent with what was expected.
Thanks for reading! Feel free to connect with me on LinkedIn.
Top comments (0)