Azure Functions + Container Instances

I have a node.js application that does some short-lived work and for years i have used crontab to execute the application on a schedule. This would run the application until termination and then the machine would wait for crontab to start the process all over again. Recently, i got an urge to fix this and started looking at Docker. Im sure everyone knows what Docker is, so im going to cut to the chase. There were a few things that crossed my mind:

I ended up going with the last approach based on the costing shown in this Azure Pricing Calculator estimate that i put together. In terms of pricing, the virtual machine, which is an Azure A0 instance weighs in at 0.75GBs of RAM and sports a single Intel Xeon E5-2630 v3 core with a price tag of $19.74 per month. Option two: running an always on container as an Azure Container Instance with 1GB of RAM and 1 vCPU would set us back $51.84 per month, with the last option of using Azure Functions to create and delete the containers on the same spec hardware coming in at $12.96 per month. It’s also worth noting that Virtual Machines are billed per hour, while Azure Container Instances are billed per second.

Container

The first thing i needed to do is actually containerize the application, which was ridiculously easy. We can start with an open source base image that already had node and python configured and craft the following Dockerfile. Everyone’s Dockerfile is going to look different, but nevertheless, this is what it took to containerize my application.

FROM beevelop/nodejs-python:latest
WORKDIR /app
COPY package.json /app
RUN npm install
COPY . /app
CMD node src/index.js

Next i needed to build the Docker image and push it into a private Docker Hub repository.

docker run -t taylorgibb/simple-sync
docker push taylorgibb/simple-sync

Creating a Service Principal

I needed this function to run under its own service principal. So the first thing i had to do was register a new application and service principal with the appropriate permissions. To do this, i needed two things, openssland the Azure cli tools.

First we needed to generate a good secret to start with, openssl proved to be a useful tool to use for this.

openssl rand -base64 24

Now i can register my application in Azure Active Directory.

az ad app create --display-name simple-sync
                 --homepage http://developerhut.co.za
                 --identifier-uris http://developerhut.co.za
                 --password $SECRET

This returned an application identifier to me, which i used to create the service principal.

az ad create --id $APPLICATION_ID

The last step was creating a resource group and assigning a role to my newly created service principal. The az account list command will give me the subscription ID i need to do the role assignment.

az group create --location westeurope
                --name simple-sync

az account list

az role assignment create --assignee http://developerhut.co.za
                          --role Contributor 
                          --scope /subscriptions/$SUBSCRIPTION_ID/resourceGroups/simple-sync

That was a lot of effort. Nevertheless, i now have a service principal that is constrained to the bounds of my resource group. In addition to better security, we also get better billing and peace of mind.

The Functions

Next we needed to write a couple of Azure functions that coud create and delete our containers on a schedule. I had never written an Azure function before, but a few searches later and i was installing the Azure Function Core Tools

npm install -g azure-function-core-tools

Once i had the tools installed, i needed to create a new Function App and then create the actual functions within the app. I am going to be creating a time based function using the JavaScript language option and will call my function provision, this is the first of the two functions i will create.

func init simple-functions
func new
func host start

I then needed to crack open the index.js file inside the provision directory and replace the boilerplate code with our own. My container is hosted in the Docker Hub registry, so you will notice that i pass in a imageRegistryCredentials parameter so that Azure knows where to get our container from. I am creating a Linux container in West Europe but the function is easy enough to change.

module.exports = function (context) {
    const AZ = require('ms-rest-azure');
    const ACI = require('azure-arm-containerinstance');

    AZ.loginWithServicePrincipalSecret(
        process.env.AZURE_CLIENT_ID,
        process.env.AZURE_CLIENT_SECRET,
        process.env.AZURE_TENANT_ID,
        (err, credentials) => {
            if (err) {
                throw err;
        }
        let client = new ACI(credentials, process.env.AZURE_SUBSCRIPTION_ID);
        client.containerGroups.createOrUpdate('simple', 'simple-containers', {
            containers: [container],
            osType: 'Linux',
            location: 'West Europe',
            restartPolicy: 'never',
            imageRegistryCredentials: [{
                "server": "index.docker.io",
                "username": process.env.DOCKER_USERNAME,
                "password": process.env.DOCKER_PASSWORD}]
        }).then((r) => {
            context.done();
        }).catch((r) => {
            context.done();
        });
    });   
 };

Next, i needed to run npm init in the provision folder so that npm would generate a package.json file. I could then added the two dependencies you see in the above code.

npm install --save azure-arm-containerinstance
npm install --save ms-rest-azure

I also needed to edit the cron expression in the function.json file. I tried a couple of online cron expression generators, but Azure didn’t like the expressions that they generated. I found this article very useful for crafting expressions. After modification, the trigger was set to fire daily, and looked as follows:

{
  "disabled": false,
  "bindings": [
    {
      "name": "startTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 0 * * *"
    }
  ]
}

Finally, i follow the above steps again and create another function called delete. Just as above, i had to run npm init to initialize the project and then install the required modules via npm with the --save flag. The contents of the delete function is below.

module.exports = function (context) {
    const ACI   = require('azure-arm-containerinstance');
    const AZ    = require('ms-rest-azure');

    AZ.loginWithServicePrincipalSecret(
        process.env.AZURE_CLIENT_ID,
        process.env.AZURE_CLIENT_SECRET,
        process.env.AZURE_TENANT_ID,
        (err, credentials) => {
            if (err) {
                throw err;
            }
            let client = new ACI(credentials, process.env.AZURE_SUBSCRIPTION_ID);
            client.containerGroups.deleteMethod('simple', 'simple-containers').then((r) => {
                context.log('Delete completed', r);
            });
    });
};

I only wanted the delete function to get executed 6 hours after the provision function had started the container, so it has a slightly different function.json definition too.

{
  "disabled": false,
  "bindings": [
    {
      "name": "endTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 6 * * *"
    }
  ]
}

At this point, i committed my code to a private Github repository

az storage account create --name simplestorage --location westeurope --resource-group simple-sync --sku Standard_LRS

az functionapp create --deployment-source-url https://github.com/taylorgibb/simple-sync
                      --resource-group simple-sync 
                      --consumption-plan-location westeurope 
                      --name simple-functions
                      --storage-account simplestorage

In the two functions above, i use environment variables to keep sensitive information from being committed to source control. By definition, environment variables, are configured in the environment itself and that means we need to add a few more things to Azure. The settings i added were generated in the first part of the article on creating a service principal, along with some fake Docker Hub credentials. It goes without saying that these values will need to be substituted with your own if you are following along.

az functionapp config appsettings set --name simple-functions
                                      --resource-group simple
                                      --settings AZURE_CLIENT_ID=XXX 
                                                 AZURE_CLIENT_SECRET=XXX
                                                 AZURE_TENANT_ID=XXX
                                                 AZURE_SUBSCRIPTION_ID=XXX 
                                                 DOCKER_USERNAME=XXX 
                                                 DOCKER_PASSWORD=XXX 

Now that all my config is complete, i used some nifty helper methods to mirror it onto my local machine so that i could test the functions locally in the future if i need to. When we run the below, it pulls all the settings into a special file called local.settings.json which lives in the root of your function app. The local.settings.json file is in the .gitignore by default and wont be committed to source control, keeping all your secrets safe.

func azure functionapp fetch-app-settings simple-functions

Thats pretty much all there was to it. At this point, i logged into the Azure portal and manually ran the provision function and verified that it created a new container instance, i then ran the delete function and ensured the container instance was removed. I ran into a couple of issues while making this, most notably with the Azure Function Core Tools there is a bug that prevents you from publishing functions from the command line if your function contains a node_modules folder. You can read more about that over here.