Deploying .NET 5 Azure functions with Pulumi and GitHub Actions

In this post I’ll show you how to deploy the .NET 5 “Out of process” azure functions using Pulumi. We’ll be using a GitHub action to build the code, which will also create the infrastructure too, then deploy the function to that infrastructure. In this example, we’ll be using a Azure Blob Storage to store the state of our Pulumi stack.

If you’d just like to view the solution, you can find the code here: martinjt/pulumi-dotnet5 (github.com)

What is Pulumi?

Pulumi is an Infrastructure as Code framework that allows you to declare your infrastructure in the same language you do your code. In this case, we’ll be writing it in C# to match the code of our function.

What are Github Actions?

Github Actions are free Build runners provided by GitHub that run against your Github repo. You currently get 2000 free build minutes for each of your personal accounts. You can use them to do anything, and we’ll be using them to build and deploy our code to Azure for free!

Pre-requisites

For this tutorial, you’ll need:

  1. An Azure Subscription to deploy to
    (You’ll need Contributor rights for Storage Accounts, Blobs, AppService/Function apps)
  2. An Azure Storage blob for storing the state
  3. Access to (or the ability to create) a service principal.
  4. Fork of the repository at https://github.com/martinjt/pulumi-dotnet5

Step 1 – Creating a Service Principal

You’ll need to have a service principal that Pulumi can use to create the resources in Azure. Behind the scenes, Pulumi is hitting the Azure REST API, and for that it needs credentials

If you have a service principal, then you can skip this part.

The easiest way I’ve found to do this is using the Azure CLI. Note that currently, the CLI is creating a Service Principal with Contributor permissions, however, there is a message saying this will not always be the case. You’ll need Contributor permissions for this.

az login
az account set -s <subscriptionId>
az ad sp create-for-rbac --name pulumi-tests

In the above commands, you’ll notice that I’m setting the subscription as default. This is something that I would recommend, but you could equally just pass the subscriptionId on the command to create the principal.

This should return you a JSON blob that looks like this:

{
  "appId": "",
  "displayName": "pulumi-tests",
  "name": "http://pulumi-tests",
  "password": "",
  "tenant": ""
}

Keep hold of this as we’ll need it in a subsequent step.

Step 2 – Hosted State file setup

We’ll be storing our Pulumi state file in Azure Blob Storage, so that will need creating manually.

Pulumi maintains an “internal” dictionary of all the resources it’s created as part of the stack, and how those map to the things it needs to create. As GitHub actions don’t have a shared place for these things, we need a persistent store for them. We’ll be using Azure Blob for this.

You’ll need a storage account (using an existing one is completely fine). I’d recommend storing this in the same subscription as the infrastructure it’s managing (i.e. if you have a subscription for Dev, test and prod, put the state files for each environment in those).

The only recommendation around creating this I would have is disabling the public access.

Screenshot of the “Enable blob public access” checkbox for blob storage

Once you have a Storage account, you’ll need a blob creating within there. Note down the names of both the storage account, and the blob as these will be need in the subsequent steps.

Step 3 – Github Secrets

For the pipeline to run, you’ll need to add a couple of secrets that will grant access to the Azure Blob storage, the Infrastructure, and also encrypt the state created by Pulumi. You’ll also need the variables for the Storage account name, and the blob name.

Within your fork, setup the following secrets in <fork> => Settings => Secrets => New Repository Secret.

Note: This works equally well with Organisation or Environment secrets if you’re using them.

Service Principal

The Github Action workflow is setup to use a secret called AZURE_PRINCIPAL

      - name: Azure Login
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_PRINCIPAL }}

This comes from the Action provided by azure, and expects the following json format:

{  
  "clientId": "<appId>",
  "clientSecret": "<password>",
  "tenantId": "<tenant>",
  "subscriptionId": "<subscriptionId>"
}

These should all be available from the principal that was setup in Step 1.

Pulumi Secret

You’ll need to provide pulumi with a “key” for which it will use to encrypt the state of your workflow. This is set as PULUMI_PASSPHRASE and can be any string

Storage Account

Next you’ll need to provide a valid storage account (that the principal supplied has access to). This is done with the STATE_STORAGE_ACCOUNT secret

Storage Blob

This is the name of the blob used for the Pulumi State file, and is set as STATE_STORAGE_BLOB. You need to ensure that the Service Principal supplied has Read, Write and List permissions to the blob.

Step 4 – Run the workflow

That’s it, you should now be able to run the workflow from the menus.

Screenshot of the Pipeline

The important bits

There are a few important things to note when creating both Azure functions and .NET 5 Azure Functions with Pulumi.

Functions Runtime

When you’re deploying an Azure Function with .NET 5, you’ll need to make sure that you set the AppSettings FUNCTIONS_WORKER_RUNTIME to dotnet-isolated

Changing Blobs on build

The solution I’ve provided uses the WEBSITE_RUN_FROM_PACKAGE AppSetting that points to a blob in Blob storage. This provides a nice separation of the code and function. However, due to the way that pulumi works, the Blob resource is not marked as updated, and therefore does not change the URLs. As the URL to the blob doesn’t change, the Function App will not pick up the new code.

To workaround this, I’ve added a DateTime to the blob’s name, so it’s updated on every build. I would recommend having this use the commit hash instead.

This does still have limitations in that old Function Apps could start errors as the old blob is being deleted.

Don’t publish your Infrastructure code

When you do a dotnet publish in your application, make sure that you target the application’s project, and not the solution. If your solution file contains both the application and the infrastructure, publishing the solution will result in around 200MB of extra libraries that you don’t need. This quickly fills up your GitHub data allowance.

Conclusion

Deploying an Azure function with Pulumi is pretty easy, adding in the complexity of Github Actions/Workflows is actually pretty easy too. There are a few incantations that you need in order for the Action to access azure, but other than that, it was fairly smooth sailing.

I hope this forking and deploying this repo makes it easier to understand what’s needed.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Built with WordPress.com.

Up ↑

%d bloggers like this: