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.
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.
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.
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.