Coursework case study

FOCUS

A student task manager deployed on Azure

A server-rendered task manager built with Flask and PostgreSQL, deployed to Azure on free-tier services. It was a solo coursework project (Cloud Application Development, Yoobee Colleges). The app is simple on purpose. The interesting part is the deployment, so that is what this write-up covers.

No longer hosted (see the last section).

View code on GitHub (opens in new tab)
App Flask, Jinja2, SQLAlchemy, Flask-Migrate
Auth Flask-Login, Werkzeug (scrypt hashing)
Database Azure Database for PostgreSQL Flexible Server, Burstable B1ms
Hosting Azure App Service F1 (Linux, Python), gunicorn
CI/CD GitHub Actions via App Service Deployment Centre
Cost control $5 monthly budget alert

Students register, log in and manage their tasks for the day, each with a status, priority and reflective notes. The dashboard shows only today's tasks. The app had to run on a cloud provider with monitoring, and I had no hard spending cap on my subscription, so keeping cost near zero shaped most decisions.

FOCUS dashboard showing Tasks for Today: two tasks, each with a Low priority badge, a date, a status dropdown set to Yet-to-do, and Edit and Delete buttons
FOCUS All Tasks page listing four tasks with Low, High and Medium priority badges, dates from 11 to 25 September, and one task marked Completed
Edit Task form with title, status, priority, date and notes fields, and Cancel and Save Task buttons

Register, log in, log out, create, edit and delete tasks, change status from a dropdown on the list, and write notes. Every task query is filtered by the current user's ID, so changing a task ID in the URL returns a 404 instead of someone else's task. I left out image upload and password changes, which were optional.

  • Pushes to main trigger a GitHub Actions workflow that builds and deploys to App Service.
  • Config (DATABASE_URL, SECRET_KEY) comes from environment variables: a .env file locally, App Service settings in production. The same code runs in both.
  • The database enforces TLS, my app connects with sslmode=require, and a firewall allows only Azure services and my own IP.
  • Tables were created by running flask db upgrade from my machine against the Azure database.

OIDC deploy auth failed. I tried to give the pipeline short-lived tokens through OpenID Connect. It failed because my GitHub account only had the older OAuth App authorisation, not the GitHub App installation OIDC needed. I switched to a publish profile stored as an encrypted GitHub secret. That works, but it does not expire, so it is the weaker option and I documented it as a known risk.

A green tick, a crashed site. The first deployment passed in GitHub Actions, but the site crashed with ModuleNotFoundError: No module named 'wsgi'. Six local commits had never been pushed, so Azure deployed the old code. The log stream showed the error straight away. The lesson: a green pipeline only proves what the pipeline checks, and a local commit is not deployed code.

Azure App Service log stream during startup, ending with gunicorn 26.2.0 starting, listening on port 8000 and booting a worker

Total spend was NZ$0.17. The App Service plan and bandwidth cost nothing, and the only charge came from a short period on a larger database size (the B2S meter) before I moved to B1ms. Storage sat at 13.42 GB of the 32 GB allowance and compute at 301 of 750 free hours.

Azure cost analysis showing a total of NZ$0.17, all from Azure Database for PostgreSQL on the B2S meter, with App Service and bandwidth at NZ$0.00
Azure free services panel: 3 services used within limit and 0 exceeded, with PostgreSQL storage at 13.42 of 32 GB and compute at 301 of 750 hours

Monitoring came from four places: the log stream, App Service metrics, database metrics, and a /health endpoint I wrote that runs SELECT 1 and reports app and database status in one request. Over seven days the site handled 28 requests with no server errors. The 7.14 second average response time is likely cold starts rather than slow code, because F1 has no Always On. On the database, memory (about 61%) was tighter than CPU (about 13%). Application Insights was not available on this tier, so I could see that a request was slow but not why.

App Service metrics over seven days: 28 requests, 0 server errors, 7.14 second average response time and 31.29 seconds of CPU time
PostgreSQL metrics over seven days: CPU averaging 12.66%, memory 60.83%, storage 13.18% and 7.87 active connections
  • F1 plan: single instance, no Always On, no SLA. Fine for coursework, not for users.
  • B1ms database: the only tier inside the free allowance, but it cannot have high availability. Storage autogrow is off, so a full disk makes the database read-only instead of growing the bill.
  • Infrastructure by clicking: I built everything in the portal, so rebuilding after a loss would be manual.

Known gaps:

  • No CSRF protection on forms
  • The next redirect parameter is not validated (open redirect)
  • No login rate limiting and no password rules
  • Task creation accepts an empty title (test T08 failed)
  • config.py falls back to default values instead of refusing to start
  • Move to infrastructure as code, Entra authentication for the database, and a much shorter gunicorn timeout than 600 seconds

The app is no longer hosted. I took it off Azure to avoid ongoing costs. The code is on GitHub.