Rob Online

Azure I love you, I love you not

One of our services, a URL shortener, is written in Python and hosted in Kubernetes. Given it's a tiny service, just two endpoints, I wanted to try moving to Serverless.

Azure Functions were my tools of choice. Start to finish it was probably 6 hours of my time, including learning some basic Terraform (and the Azure provider). Here's how it went.

The functions

I wanted to host it as cheaply as possible, if not free. The service is low-traffic, and I fancied writing it in Go instead of Python, for a change. That last one was instantly blown out of the water - Azure Functions support C#, Javascript, something (I forget, but it's not Go; it's probably Java) and Python. Fine. I'll stick with Python. The documentation looks really good, and I walk through an initial tutorial with no problems. The func command is pretty neat, and the locally running function is very fast to start and respond, as one would hope.

I have to rewrite a bit of code. The old service was written in Flask, with Flask-SQLAlchemy. While it's possible to host totally custom code in an Azure Function, as a Docker image, I wanted the purest, and cheapest, Azure Function experience. So no Flask, and vanilla SQLAlchemy. The documentation was confusing here - it recommended an odd folder naming convention (e.g. here and here), and skipped over some useful points. Importing shared code is done via relative folder imports. Installing dependencies can only be done with a requirements.txt file - no nice Poetry packaging here!

While a lot of effort's been put in, some of the inconsistencies indicate the Python side of things hasn't had the chance to benefit from much product management. But despite all that, the tech's good and it works locally. Cool!

The infrastructure

The documentation here is good, and covers using Powershell (barf) and the Azure commandline tool az. The latter is pretty good, and following the basic tutorial to get things up and running is good - I was making infrastructure very fast.

Then I found that Azure has good Terraform support. Diving into Terraform a little (there's a good Pluralsight course, and the docs are pretty great), I rewrote what I'd done in that format. Much better.

By this point, I had the following infrastructure:

  • Resource group
  • Storage (function apps all need storage)
  • App service (they need one of these as well)
  • Function app
  • Key Vault
  • App Insights (this is how you see logs in an Azure Function)
  • API Management (Azure's API Gateway)

It's hard to overstate how magical this declarative syntax is the first time you use it. It's not perfect, but then I think I tried to do too much with it. Creating a user in Postgres and inserting their credentials into the Key Vault, and retrieving them in the function app as environment variables may have been a step too far.

But, that last bit is pretty easy and can all be done in Terraform. It's just not particularly well documented.

First, create a managed service identity for your function app - this is pretty great. It creates a service principal in Azure's Active Directory without you having to think of an email address for a service. Then allow that principal to log into the Key Vault, with get permissions. Make sure your Terraform user has set permissions, and has set in the relevant secrets. Your Key Vault needs to have firewall permissions opened up - it can't just trust Azure services, for some reason. Finally, add into the environment variable settings of the function app this odd syntax for the variable value, which will look inside Key Vault for the actual value: @Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.my_key_vault_secret.id}). The ${} variable is a Terraform one - it outputs the full URI to the secret.

Pretty good, right? It'd be even better if the app could automatically log into Postgres, but that would require code changes in the app as well, making it less portable, or moving to C# and Entity Framework, which I can't be bothered to do. So I'm happy.

I haven't enabled the API Gateway yet - when I do, I'll lock down the network and add in rate limiting to make it hard to guess shortened URLs.

Publishing the app to Azure

So I have an app that runs locally, and some infrastructure waiting to do useful things. All I have to do now is publish the app, and see it all working!

Of course, it's not that easy. Turns out, for some reason (at time of writing), the function won't build with SQLAlchemy as a dependency. It's possible this can work in a different setup; for example, with a function app that is always on in a Docker image, rather than a function that only fires up on use, but I'd really like to implement the latter.

My journey stops, or at least pauses, here. Other than the showstopper, I really like what I'm seeing, even though it's a little rough around the edges.

Thanks for reading. I hope anyone who's trying to do something similar got something useful out of it, even if that someone is just future me!