Deploying Azure Functions with Tekton CI/CD

Tekton is a set of shared, open source components for building CI/CD systems and a part of the new Continuous Delivery Foundation. While still in early stages, there is significant investment in the project due to its ability to integrate natively with the Kubernetes ecosystem.

In this example, I will be demonstrating the flexibility that Tekton can provide by deploying an application to Azure Functions. The goal of this exercise will be to create a CI/CD pipeline that tests the source code, configures the necessary Azure resources, then updates the deployed function from the latest commit. I will be using the Azure Functions Javascript runtime, Terraform to configure Azure, then the Azure Functions Core Tools binary to deploy the Function.

Source code for this post can be found on Github.

Tekton

Tekton is built on top of a set of Kubernetes custom resources that allow for composability. The Pipeline is the top level entrypoint, which is composed of inputs and Tasks. Each task can depend on its own set of inputs as well as PipelineResources. The resources that are supported at this time are fairly limited, but a full list can be found here. I will demonstrate the power of Tekton using just a single resource, the Git repository resource, combined with Kubernetes namespace objects in order to create a flexible pipeline for deploying serverless applications to Azure Functions.

Build and Test Application

The first step is to create an Azure FunctionApp locally and a Function within that directory. In order to do this, run the following

func init --worker-runtime node
func new --language JavaScript --name ReturnMyName

Then, select the “HTTP trigger” option. This creates a node structure within the source directory with package.json, host.json, and local.settings.json files at the root. The local.settings.json file is intended to hold secrets for local development, don’t commit this file to source control!

In order to test your repository, use the jest npm package

npm install --save-dev jest

The Jest library allows us to mock interactions with any externally invoked functions, but in this case testing the exported function is fairly simple. In this case, we just pass in a request object and validate the values on the response.

const httpFunction = require('./index');
const context = {};
 
test('Http trigger should return known text', async () => {
 
    const request = {
        query: { name: 'Bill' }
    };
 
    await httpFunction(context, request);
 
    expect(context.res.body).toEqual('Awesome CI/CD Bill');
});

Now, create a Tekton Task to run this test as the first step of the pipeline. The git resources get checked out to a directory underneath `/workspace` which is shared between all steps of the pipeline. This shared nature allows us to run multiple commands against files within the same directory, using an emptyDir volume to share the results of one step with the inputs on the next.

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: npm-test
  namespace: azure-functionapp
spec:
  inputs:
    resources:
      - name: functionapp-git
        type: git
  steps:
    - name: install
      image: node:10
      workingDir: /workspace/functionapp-git/app
      command:
      - npm
      args:
      - install
    - name: test
      image: node:10
      workingDir: /workspace/functionapp-git/app
      command:
      - npm
      args:
      - test

The Tekton dashboard can provide a good view of what is happening within this task.

NPM Test Tekton Task Output

This completes the testing component for the pipeline

Deploy Infrastructure Components

In order to create an Azure Function, we need to configure our Azure Account with the specific resources that Azure requires for Function Apps. This example will use the following

  • Resource Group – For organizing Azure resources
  • App Service Plan – Environment for running the Azure Function
  • Storage Account – Storage for Function source code and other components of Function execution

These resources can all be managed by the Azure Terraform provider. I will use a separate resource group and storage account to store the Terraform state for use in the pipeline, defined in the terraform backend

terraform {
  backend "azurerm" {
    resource_group_name  = "terraform-mgmt"
    storage_account_name = "terraformmgmt"
    container_name       = "tfstate"
    key                  = "functionapp-demo.terraform.tfstate"
  }
}

In keeping with the theme of re-usability of our Tekton pipelines, I created variables for two input parameters, resource_group_name and function_name, that can be passed in during terraform apply. Since we are executing a simple example all the files are in a single repo together, but this pipeline can easily be extended to as many resource groups and functions as desired.

In order to deploy our node function, we need to use the app_settings block to set the Azureworker runtime name and the node version to use.

resource "azurerm_function_app" "test" {
  ...
  app_settings = {
    FUNCTIONS_WORKER_RUNTIME      = "node"
    WEBSITE_NODE_DEFAULT_VERSION  = "10.14.1"
  }
}

Now, create a Tekton task for deploying this infrastructure out to Azure. You can see this task once again relies on the git resource, but this time we add the two defined input parameters of resource_group_name and function_name.

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: azure-deploy-infrastructure
  namespace: azure-functionapp
spec:
  inputs:
    params:
      - name: resource_group_name
        type: string
      - name: function_name
        type: string
    resources:
      - name: functionapp-git
        type: git
...

Then, when terraform apply is invoked, we pass those arguments into the command using the following Task configuration.

command:
- terraform
args:
- apply
- -var 
- resource_group_name=$(input.params.resource_group_name) 
- -var
- function_name=$(inputs.params.function_name)

The Tekton controller automatically replaces these input value with the parameters from the Pipeline object.

In order to deploy to Azure, we must provide credentials for the Azure service account used to deploy. This is where we can leverage the power of Kubernetes to draw upon the resources in the Kubernetes namespace, we set the environment variables ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID, and ARM_SUBSCRIPTION_ID from keys in a secret named azure-credentials which is applied to the Kubernetes namespace. These variables allow the Terraform Azure provider to authenticate with Azure on each run.

Again, we can use the Tekton dashboard to view the logs and status of these steps.

Terraform Apply Azure

When this completes, the Azure components have been created and we are ready to deploy our function code.

Update Function in Azure

Now that the Function App object is created in the Azure portal, we can use Azure Function Core Tools to deploy the function from our source code. We must use a docker image that contains both the Azure CLI and the Function Core Tools, so I created a custom Docker image based on Ubuntu that installs both of these tools. I also created a simple shell script that uses the Azure CLI to login and the Azure Function Core Tools binary to deploy the application from the app directory of the source, defined by the Tekton task.

Now, deploying the function is as simple as invoking the shell script within the Task with appropriate environment variables. Just as we injected environment variables for terraform, we use the same azure-credentials secret in the namespace to set environment variables used by the script. First, run an npm production install to download the node dependencies, ignoring the test dependencies, then use the constructed shell script to login to the Azure portal and deploy the application code.

apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
  name: azure-deploy-app
  namespace: azure-functionapp
spec:
  inputs:
    resources:
      - name: functionapp-git
        type: git
    params:
      - name: function_name
        type: string
  steps:
    - name: install
      image: node:10
      workingDir: /workspace/functionapp-git/app
      command:
      - npm
      args:
      - install
      - --only=prod
    - name: deploy
      image: jmcshane/azure-func-cli:0.2
      workingDir: /workspace/functionapp-git/app
      command:
      - sh
      args:
      - /deploy.sh
      env:
      - name: FUNCTION_NAME
        value: "$(inputs.params.function_name)"
      - name: USERNAME
        valueFrom:
          secretKeyRef:
            name: azure-credentials
            key: username
      - name: PASSWORD
        valueFrom:
          secretKeyRef:
            name: azure-credentials
            key: password
      - name: TENANT
        valueFrom:
          secretKeyRef:
            name: azure-credentials
            key: tenant

The Tekton dashboard displays the function getting updated via the Azure Function CLI.

Azure Function Deployment

Now our Function has been deployed to Azure!

Putting the Pieces Together

We have created three Tekton Task objects to execute the steps of our pipeline, so now we put those tasks together into a Pipeline. The Pipeline will aggregate all the downstream parameters and resources into a single entrypoint. The Pipeline will create all the tasks in an appropriate order, which we can specify with a runAfter declaration. In this case, we are waiting to update the app until after the code has been tested and the infrastructure created.

apiVersion: tekton.dev/v1alpha1
kind: Pipeline
metadata:
  name: functionapp-pipeline
spec:
  resources:
  ...
  params:
  ...
  tasks:
    - name: npm-test
      taskRef:
        name: npm-test
      ...
    - name: deploy-infrastructure
      taskRef:
        name: azure-deploy-infrastructure
      ...
    - name: deploy-functionapp
      taskRef:
        name: azure-deploy-app
      runAfter:
      - npm-test
      - deploy-infrastructure
      ...

See this file in Github for the full Pipeline spec. Once this Tekton Pipeline object is applied to the namespace, we can use the dashboard to create a PipelineRun, an actual execution of the pipeline, from the Pipeline object. This will generate a unique ID for the PipelineRun and create all the downstream Task objects dynamically based on the Task graph that gets generated. Each PipelineRun can define values for the input parameters and the Pipeline git resource, as seen in the Create dialogue.

Pipeline Run Create Dialogue

Once we create the PipelineRun, the tasks execute in the order defined in the pipeline file and appear in the UI with the most recent execution at the top. This pipeline executes the three steps of testing, deploying infrastructure, then finally deploying the Function App to Azure.

Full Tekton PipelineRun View

For more information about the tools used in the project, check out the following

  1. Tekton Pipeline
  2. Tekton Dashboard
  3. Azure Functions Overview
  4. Azure Terraform Provider
Leave a Reply

Your email address will not be published. Required fields are marked *

*

*