Skip to content

Tech-Stack for hosting small apps on Azure for free

I’m sometimes doing web apps for hobby/personal projects which I like to host online just very low cost or for free. I have the following set-up in mind for quite some time and finally took the time to explore it and got it run. In this post I write my findings.

Summary

With Azure Functions and Azure Storage Accounts combined, you get a powerful and very cheap or free stack for smaller projects running SPA’s, .Net backends and storing various kinds of data.

As always, it needs tome time to figure the nasty details on how to connect the building blocks. Also have a look at the limitations. You also must be open on new tech and not just require good old MS-SQL server.

My requirements

Basically my wish list from the technical side with focus on web-frontend and backend. In the future, maybe native mobile-apps will join.

  • Backend
    • Modern .Net (Version 6/7/x)
    • Supported facades: API’s (HTTP), scheduled Jobs (Timer), message handlers (Queues/Events)
    • Support storages: no-SQL, blobs, queues, optional: SQL/relational, optional: others
  • Frontend
    • Support for SPA frameworks (Rect/Next or others)
    • Custom domains
    • Free to use any CSS-frameworks, component-libraries etc.
    • Frontend can call the backend API (HTTPS, CORS, TLS, …)
  • Mobile Apps
    • Possibility to communicate to the backend for native mobile apps in the future

Choose tech-stack

To host projects with the above stack with low or no costs, I came up with the following:

  • Repository: GitHub
    • Free for personal, small or open-source projects
  • CI / CD: GitHub Actions
    • Some usage for free included in GitHub repo.
  • Backend: Azure Serverless Function app(s) written in .Net C# 7
    • Supports HTTP, timers, queues, storage and others as triggers
    • In the consumption plan, there is a decent compute amount included for free: 400’000 GB-s.
      Tip: Set a daily usage consumption on max. 13’000 GB-s/day to make sure you stay in free tier. Change this to our app and budget needs.
  • Data: Azure Table Storage (part of Azure Storage Account), an option is Azure Cosmos DB with its free tier (<1000 RU/s, <25 GB)
  • Blobs/Fles: Azure Blob Storage (part of Azure Storage Account)
  • Messaging: Azure Storage Queue (part of Azure Storage Account)
  • Frontend: Azure Blob Storage Static Website (part of Azure Storage Account)
  • Telemetry/Monitoring: Azure Application Insights (Azure Monitor & Log-Analytics)

Azure Storage Accounts

All of my storage requirements for small projects are covered by Azure Storage Accounts. This service is very cheap (eg. €0.017 per GB/moth for Hot-Storage, €0.009 per GB/moth for Cold-Storage)! That’s basically free for most small projects.

Azure Storage Accounts is the home for:

  • Blob storage: storing files/buckets/etc. in hierarchical containers. Optionally, you can also use file-sharing to upload and download files from Windows Explorer / Mac Finder – maybe interesting for some special use-cases.
  • Table storage: simple and highly scalable NoSQL data, with some restrictions on indexing and querying. Check the limitations and best-practice for partition- and row-keys before using it. Optionally supported by Azure Functions with input- and output-bindings.
  • Queue: Simple message-queues. Supported by Azure Functions as queue-/message-triggers.

Azure Static Web Apps

Because the combination of Azure Functions and Azure Storage Accounts is very powerful to host backends and SPA’s, Microsoft is building a dedicated service around this called Azure Static Web Apps. It simplifies the configurations and gives some advanced options but also introduce some limitations.

A bonus is that you also get a Node-JS server where you can do things like server-side rendering.

Limitations

One limitation that hit me, was that the Azure Functions only supporting HTTP triggers; timers, queues and other triggers are not (yet?) supported.

Another limitation: the free plan does not support “bring your own Functions backend” and therefore you can not work around the above limit by bringing our own Azure Functions instance.

None of these limitations you have when using your own custom Azure Function instance.

Resulting Azure Resource Group

To give you an idea, here is what my Azure Resource Group looks like. My PoC is analyzing free parking spaces in the City of Zürich. Don’t look too close on my naming – it’s just a proof of concept 😉.

Some points of interest:

  • Custom Azure Functions App to get timer support
  • Custom Azure Storage Account to host my tables and the static website (SPA). Separate from the Functions own internal Storage Account.
  • Standard Application Insights instance

Implementation

I don’t like to go into every detail here, but some tips on things I struggled with.

Secure access to data

To keep the data save, I restricted the access to my Azure Table Storage using an Azure Managed Identity. It needed the “Storage Table Data Contributor” role.

Static web app

In Azure Storage Account there is a menu-item called “Static Website”. Enable it to get the required “$web” blob container. Place your SPA files and assets in this container – it is your web-root. See below for my GitHub Actions workflow on how you deploy it into this container.

CORS (Browser Security)

It took me a while until I figured where to add the CORS headers. They are needed because with this setup, your backend and your web-hosting (SPA) use different domains (=origins).

In the Azure Functions app, there is “CORS” menu-item. Add the domain / URL of your SPA app.

In the blob storage (static website) there are CORS headers too. You’ll find it under “Resource Sharing (CORS)”. Add the URL / domain of your Azure Functions backend in there.

CI / CD with GitHub Actions

Deploying the Azure Functions app is mostly using the generated GitHub Actions YAML file with a few tweaks like the path filters in the trigger. I set them so only backend-changes or changes to the backends CI/CD, trigger a backend build&deploy.

name: Backend - Build and deploy

on:
  push:
    branches:
      - main
    paths:
      - src/ParkingZuerichAnalytics/**
      - .github/workflows/backend_parkingzuerichanalytics.yml
  workflow_dispatch:

env:
  AZURE_FUNCTIONAPP_PACKAGE_PATH: './src/ParkingZuerichAnalytics/ParkingZuerichAnalytics.DataGathering'
  DOTNET_VERSION: '7.0.x'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 'Checkout GitHub Action'
        uses: actions/checkout@v2

      - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: 'Resolve Project Dependencies Using Dotnet'
        shell: bash
        run: |
          pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
          dotnet build --configuration Release --output ./output
          popd

      - name: 'Run Azure Functions Action'
        uses: Azure/functions-action@v1
        id: fa
        with:
          app-name: 'ParkingZuerichAnalytics'
          slot-name: 'Production'
          package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output'
          publish-profile: ${{ secrets.AZUREAPPSERVICE_PUB.... }}

Deploying the NextJS SPA was a little more effort. Important is the fact that you don’t get a Node server for your frontend. So you have to use the static export of NextJS. Depending on the lifetime of your data you also need to ‘use client’ so the client (Browser) will fetch the data, not the NextJS build-process.

For a tutorial on how to deploy the static website, see here.

name: App - Build and deploy

on:
  push:
    branches:
      - main
    paths:
      - src/parkingzuerich-app/**
      - .github/workflows/app_parkingzuerichanalytics.yml
  workflow_dispatch:
    
defaults:
  run:
    working-directory: ./src/parkingzuerich-app

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout GitHub Action
        uses: actions/checkout@v2
        
      - name: Install NodeJS
        uses: actions/setup-node@v1
        with:
          node-version: 18
          
      - name: Install packages
        run: npm install
        
      - name: Build static site
        run: npm run build
        
      - name: Login to Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Upload to blob storage
        uses: azure/CLI@v1
        with:
          inlineScript: |
            az storage blob upload-batch --account-name parkingzuerichapp  --auth-mode key -d '$web' -s ./src/parkingzuerich-app/out

      - name: Logout from Azure
        run: 
          az logout
        if: always()

Alternatives

  • Hosting Next JS SPA: Vercel has a free plan. Interesting if you use Node for your backend.
  • Google Cloud (GCP): very cheap options for messaging (Google PubSub) and no-sql data (Google Firestore).

Closing

If you open your mind to new tech, you can get really cheap hosting for your smaller projects. Just stay under the radar to not trigger the expensive limits. Make a PoC if things work for you.

Hope this helps and inspires!

Leave a comment