Howdy,Kloudy!
November 28, 2022

GitLab CICD To Deploy Azure Functions

Posted on November 28, 2022  •  8 minutes  • 1524 words  
Views
Table of contents

Introduction

There are many ways to deploy Azure resources.

The most commonly used method to provision Azure resources is with Infrastructure as Code, and the tool is your choice. I recommend using Bicep. But, for this blog post, I used Terraform because I have to demonstrate the remote state backend configuration.

Design (Initial Draft)

design-initial-draft

Merge & Branch Pipeline (High Level Flow)

merge-branch-pipeline

Resource Visualizer

PRD - Production Environment

resource-visualizer

Prerequisites

Requirement

Solution

Our source code folder structure is as shown below

📦icollabrains
┣ 📂src
┃ ┣ 📂iHome
┃ ┃ ┣ 📜function.json
┃ ┃ ┗ 📜run.ps1
┃ ┣ 📜.gitignore
┃ ┣ 📜host.json
┃ ┣ 📜local.settings.json
┃ ┣ 📜profile.ps1
┃ ┗ 📜requirements.psd1
┣ 📂terraform
┃ ┣ 📜backend.tf
┃ ┣ 📜main.tf ┃ ┣ 📜outputs.tf
┃ ┣ 📜providers.tf
┃ ┗ 📜variables.tf
┣ 📜.gitlab-ci.yml
┗ 📜README.md

project-structure

A solution is not only to solve the problem. It should be easy to adapt! With that consideration, I would like to walk through the proof of concept deployment in simple steps.

Terraform Backend Configuration

Gitlab provides a Terraform HTTP backend to store the state files with the below essential features

backend.tf

terraform {
 backend "http" {
   address  = "${TF_ADDRESS}"
   username = "${TF_USERNAME}"
   password = "${TF_PASSWORD}"
 }
}
VariableValue
TF_ADDRESShttps://gitlab.com/api/v4/projects/[PROJECT_ID]/terraform/state/[ENVIRONMENT]
TF_USERNAMEGITLAB_USER_NAME
TF_PASSWORDGITLAB_PAT_TOKEN

🚀 PROJECT_ID = ${CI_PROJECT_ID} (GitLab Predefined Variables)
🚀 ENVIRONMENT = ${ENVIRONMENT} (Var in YAML)

Terraform Template

List of resources to provision

main.tf

resource "azurerm_resource_group" "resource_group" {
  name     = var.resource_group_name
  location = var.location
}


resource "azurerm_storage_account" "storage_account" {
  name                     = var.storage_account_name
  resource_group_name      = azurerm_resource_group.resource_group.name
  location                 = azurerm_resource_group.resource_group.location
  account_tier             = var.storage_account_tier
  account_replication_type = var.storage_account_replication_type
}

resource "azurerm_app_service_plan" "app_service_plan" {
  name                = var.app_service_plan
  location            = azurerm_resource_group.resource_group.location
  resource_group_name = azurerm_resource_group.resource_group.name
  sku {
    tier = var.app_service_plan_tier
    size = var.app_service_plan_size
  }
}

resource "azurerm_windows_function_app" "function_app" {
  name                       = var.function_app_name
  resource_group_name        = azurerm_resource_group.resource_group.name
  location                   = azurerm_resource_group.resource_group.location
  storage_account_name       = azurerm_storage_account.storage_account.name
  storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key
  service_plan_id            = azurerm_app_service_plan.app_service_plan.id
  site_config {
    application_stack {
      powershell_core_version = "7.2"
    }
    use_32_bit_worker = false
    always_on         = true
  }
}

Variables

variables.tf

variable "resource_group_name" {
  type        = string
  description = "(optional) describe your variable."
}


variable "location" {
  type        = string
  description = "(optional) describe your variable."
}

variable "storage_account_name" {
  type        = string
  description = "(optional) describe your variable."
}

variable "storage_account_tier" {
  type        = string
  description = "(optional) describe your variable"
}

variable "storage_account_replication_type" {
  type        = string
  description = "(optional) describe your variable"
}

variable "app_service_plan" {
  type        = string
  description = "(optional) describe your variable"
}

variable "app_service_plan_tier" {
  type        = string
  description = "(optional) describe your variable"
}

variable "app_service_plan_size" {
  type        = string
  description = "(optional) describe your variable."
}

variable "function_app_name" {
  type        = string
  description = "(optional) describe your variable."
}

Variable Values

VariableValue
resource_group_namergp-{Function_App_Name}-FunctionAppNam**e−{Environment}
locationukwest (CICD Variables - GitLab)
storage_account_name{Function_App_Name}stgFunctionAppNamestg{Environment}
storage_account_tierStandard (CICD Variables - GitLab)
storage_account_replication_typeLRS (CICD Variables - GitLab)
app_service_plan{Function_App_Name}-FunctionAppNam**e−{Environment}
app_service_plan_tierStandard (CICD Variables - GitLab)
app_service_plan_sizeS1 (CICD Variables - GitLab)
function_app_name{Function_App_Name}-FunctionAppNam**e−{Environment}

The below values are declared in the YAML file.

CICD

Variables

variables:
  Environment: dev
  Function_App_Name: "icollabrains"
  TF_ADDRESS: "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${Environment}"
  KICS_VERSION: "1.2.4"

Before Script Template

.before_script_template:
  image:
    name: hashicorp/terraform:latest
    entrypoint:
      - "/usr/bin/env"
      - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  before_script:
    - rm -rf .terraform
    - terraform init -backend-config=address=${TF_ADDRESS} -backend-config=lock_address=${TF_ADDRESS}/lock -backend-config=unlock_address=${TF_ADDRESS}/lock -backend-config=username=${TF_USERNAME} -backend-config=password=${TF_PASSWORD} -backend-config=lock_method=POST -backend-config=unlock_method=DELETE -backend-config=retry_wait_min=5

Terraform initialized with the below parameters

Lots of variables, right? Yeah, but not complex! These vars are to define the backend configuration with address, lock address, credentials, lock method, unlock method and retry wait time.

Stages

stages:
  - kics-scan
  - kics-result
  - validate
  - plan
  - apply
  - deploy
  - functionaltest
  - destroy

For convenience, I defined eight simple stages

KICS Scan

kics-scan:
  stage: kics-scan
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  image: alpine
  before_script:
    - apk add --no-cache libc6-compat
    - wget -q -c "https://github.com/Checkmarx/kics/releases/download/v${KICS_VERSION}/kics_${KICS_VERSION}_linux_x64.tar.gz" -O - | tar -xz --directory /usr/bin &>/dev/null
  script:
    - kics scan -q /usr/bin/assets/queries -p ${PWD} -o ${PWD}/kics-results.json
  artifacts:
    name: kics-results.json
    paths:
      - kics-results.json

KICS Scan Result

kics-result:
  stage: kics-result
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  before_script:
    - export TOTAL_SEVERITY_COUNTER=`grep '"total_counter"':' ' kics-results.json | awk {'print $2'}`
    - export SEVERITY_COUNTER_HIGH=`grep '"HIGH"':' ' kics-results.json | awk {'print $2'} | sed 's/.$//'`
    - export SEVERITY_COUNTER_MEDIUM=`grep '"INFO"':' ' kics-results.json | awk {'print $2'} | sed 's/.$//'`
    - export SEVERITY_COUNTER_LOW=`grep '"LOW"':' ' kics-results.json | awk {'print $2'} | sed 's/.$//'`
    - export SEVERITY_COUNTER_INFO=`grep '"MEDIUM"':' ' kics-results.json | awk {'print $2'} | sed 's/.$//'`
  script:
    - |
      echo "TOTAL SEVERITY COUNTER: $TOTAL_SEVERITY_COUNTER
      SEVERITY COUNTER HIGH: $SEVERITY_COUNTER_HIGH
      SEVERITY COUNTER MEDIUM: $SEVERITY_COUNTER_MEDIUM
      SEVERITY COUNTER LOW: $SEVERITY_COUNTER_LOW
      SEVERITY COUNTER INFO: $SEVERITY_COUNTER_INFO"
      if [ "$SEVERITY_COUNTER_HIGH" -ge "1" ];
        then echo "Please fix all $SEVERITY_COUNTER_HIGH HIGH SEVERITY ISSUES";
      fi            
  dependencies:
    - "kics-scan"

Terraform Validate

validate:
  extends: .before_script_template
  stage: validate
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes: 
        paths:
          - "terraform/**/*"
  script:
    - terraform validate
  dependencies:
    - "kics-scan"

Terraform Plan

plan:
  environment:
    name: $Environment
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
      changes: 
        paths:
          - "terraform/**/*"
  extends: .before_script_template
  stage: plan
  script:
    - |
      terraform plan \
      -var="resource_group_name=rgp-${Function_App_Name}-${Environment}" \
      -var="location=${location}" \
      -var="storage_account_name=${Function_App_Name}stg${Environment}" \
      -var="storage_account_tier=${storage_account_tier}" \
      -var="storage_account_replication_type=${storage_account_replication_type}" \
      -var="app_service_plan=${Function_App_Name}-${Environment}" \
      -var="app_service_plan_size=${app_service_plan_size}" \
      -var="app_service_plan_tier=${app_service_plan_tier}" \
      -var="function_app_name=${Function_App_Name}-${Environment}" \
      -out "plan.cache"            
  artifacts:
    paths:
      - "terraform/plan.cache"
  dependencies:
    - validate

Terraform Apply

apply:
  environment:
    name: $Environment
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
      changes: 
        paths:
          - "terraform/**/*"
  extends: .before_script_template
  stage: apply
  artifacts:
    paths:
      - "terraform/plan.cache"
  script:
    - terraform apply -input=false plan.cache
  dependencies:
    - plan

Deploy Azure Functions

deploy:
  environment:
    name: $Environment
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
      changes: 
        paths:
          - "src/**/*"
  stage: deploy
  image: python:3.9
  script:
    - pwd
    - apt-get update; apt-get install curl
    - curl -sL https://aka.ms/InstallAzureCLIDeb | bash
    - apt-get install curl && curl -sL https://deb.nodesource.com/setup_12.x | bash
    - apt-get install nodejs
    - npm install -g azure-functions-core-tools@4 --unsafe-perm true
    - az login --service-principal -u $ARM_CLIENT_ID -p $ARM_CLIENT_SECRET --tenant $ARM_TENANT_ID
    - func azure functionapp publish "${Function_App_Name}-${Environment}" --powershell --prefix src/

Az Functions Functional Test

functionaltest:
  environment:
    name: $Environment
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
      changes: 
        paths:
          - "src/**/*"
  stage: functionaltest
  script:
    - apt-get update
    - apt-get install curl -y
    - |
      if [ "$(curl https://${Function_App_Name}-${Environment}.azurewebsites.net/api/iHome?name=Chen | grep Hello)" ]; then
        echo "success"
      else 
        exit 1
      fi            
  dependencies:
    - "deploy"

Destroy

destroy:
  when: manual
  environment:
    name: $Environment
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
  extends: .before_script_template
  stage: destroy
  script:
    - |
      terraform destroy \
      -var="resource_group_name=rgp-${Function_App_Name}-${Environment}" \
      -var="location=${location}" \
      -var="storage_account_name=${Function_App_Name}stg${Environment}" \
      -var="storage_account_tier=${storage_account_tier}" \
      -var="storage_account_replication_type=${storage_account_replication_type}" \
      -var="app_service_plan=${Function_App_Name}-${Environment}" \
      -var="app_service_plan_size=${app_service_plan_size}" \
      -var="app_service_plan_tier=${app_service_plan_tier}" \
      -var="function_app_name=${Function_App_Name}-${Environment}" \
      -auto-approve            

References

Azure Functions Azure Functions Core Tools Terraform GitLab Predefined Variables GitLab Terraform State

Summary

Congratulations, you have successfully deployed the serverless solution. Now, you know to create a CICD pipeline in GitLab to deploy and destroy Azure functions. In my upcoming blog post, I have plans to cover the below

🚀 Implement a DevOps way to handle the enhancements.
🚀 Implement Azure Traffic Manager for high availability.
🚀 Blue Green Deployment.

Your feedback is highly appreciated. Feel free to send your comments to my ✉ inbox .

Social Networking

Let us stay connected to learn, share and grow!