Deploy to Azure Container Instance from Azure Container Registry using a CI/CD Azure DevOps Pipeline and Terraform

I am going to show using an Azure DevOps Pipeline and Terraform how you can deploy to your Azure Container Instance and setting up the pipeline to build a new Image and enabling CI/CD when you push to the repository – the pipeline will build a new image and deploy the updated image to the Azure Container Instance.

The Azure DevOps Pipeline

The Azure DevOps Pipeline has four stages:

  • terraform_base will deploy Azure Resource Group tamops-aci-rg & Azure Container Registry tamopsciacr that will hold the Images that are built and pushed at the next stage
  • Build and push stage will build and push an example application to the Azure Container Registry that has been created above
  • terraform_aci will deploy an Azure Container Instance tamopsciacr with the image that was built in the previous stage
  • terraform_destroy will be used to destroy both terraform stages

Prior to running Terraform, please follow the requirements in this blog post “Setting up Azure DevOps to begin deploying Terraform and configuring an Azure Storage Account for Terraform remote state”

Azure DevOps Pipeline Setup

During the first run of the pipeline – it is ran in stages; due to some constraints required from the previous pipeline as mentioned above in the four stages

Directory setup of Azure DevOps Repo

Docker-to-Azure-Container-Instance
    └──aspnet-core-dotnet-core
    └──templates
       └── docker.yaml
       └── terraform-apply.yaml
       └── terraform-destroy.yaml
    └──terraform
       └── main.tf
       └── providers.tf
       └── variables.tf
    └──terraform-aci
       └── main.tf
       └── providers.tf
       └── variables.tf
    └──tfvars
       └── production
          └── production.tfvars
    └── azure-pipeline.yaml

    

In the above directory setup:

GitHub repository for example code

Creating the Azure DevOps pipeline

Now lets build the first stage terraform_base

I have added all the variables required for all pipelines in this initial stage (Before running – ensure you have all the terraform folders in place too)

name: $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)

trigger: none

pr: none

parameters:

  - name: Action
    displayName: Action
    type: string
    default: 'Apply'
    values:
    - Plan
    - Apply
    - Destroy

variables:
  - name: backendServiceArm
    value: 'thomasthorntoncloud'
  - name: backendAzureRmResourceGroupName
    value: 'thomasthorntoncloud'
  - name: backendAzureRmStorageAccountName
    value: 'thomasthorntontfstate'
  - name: backendAzureRmContainerName
    value: 'dockertoaci'
  - name: backendAzureRmKey
    value: 'terraform.tfstate'
  - name: backendAzureRmKeyAci
    value: 'terraform-aci.tfstate'
  - name: environment
    value: 'production'
  - name: terraform_version
    value: '1.0.10'
  - name: action
    value: ${{ parameters.Action }}
  - name: repository
    value: 'aci'
  - name: dockerfile
    value: '$(Build.SourcesDirectory)/aspnet-core-dotnet-core/Dockerfile'
  - name: containerRegistry
    value: 'tamopsciacr'

stages :   
  - stage: terraform_base
    condition: ne('${{ parameters.Action }}', 'Destroy')
    jobs:
      - template: templates/terraform-apply.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKey }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}

Successfully running this stage, will deploy the base resources as below

Docker Registry Service Connection creation

With the ACR deployed, prior to adding the stage Build and push stage we will now create a Docker Registry service connection within Azure DevOps

Inside Azure DevOps -> Project settings -> Service Connections -> Docker Service Connection

Select relevant subscription & newly created Azure container registry

I will create the service connection with name: tamopsimagesacr – this will be referenced within the pipeline that will be created

Add Build and push stage

Now that the Service connection is created for the Azure Container Registry – lets add the second stage to the pipeline

name: $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)

trigger: none

pr: none

parameters:

  - name: Action
    displayName: Action
    type: string
    default: 'Plan'
    values:
    - Plan
    - Apply
    - Destroy

variables:
  - name: backendServiceArm
    value: 'thomasthorntoncloud'
  - name: backendAzureRmResourceGroupName
    value: 'thomasthorntoncloud'
  - name: backendAzureRmStorageAccountName
    value: 'thomasthorntontfstate'
  - name: backendAzureRmContainerName
    value: 'dockertoaci'
  - name: backendAzureRmKey
    value: 'terraform.tfstate'
  - name: backendAzureRmKeyAci
    value: 'terraform-aci.tfstate'
  - name: environment
    value: 'production'
  - name: terraform_version
    value: '1.0.10'
  - name: action
    value: ${{ parameters.Action }}
  - name: repository
    value: 'aci'
  - name: dockerfile
    value: '$(Build.SourcesDirectory)/aspnet-core-dotnet-core/Dockerfile'
  - name: containerRegistry
    value: 'tamopsciacr'

stages :   
  - stage: terraform_base
    condition: ne('${{ parameters.Action }}', 'Destroy')
    jobs:
      - template: templates/terraform-apply.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKey }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}

  - stage: Build
    dependsOn: [terraform_base]
    displayName: Build and push stage
    jobs:
    - template: templates/docker.yaml
      parameters:
        repository: ${{ variables.repository }}
        dockerfile: ${{ variables.dockerfile }}
        containerRegistry: ${{ variables.containerRegistry }}

A successful run of the build stage will build and upload the image to the Azure Container Registry as below:

Awesome! Now lets add in the final deployment stage terraform_aci and terraform destroy stage

name: $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)

trigger: none

pr: none

parameters:

  - name: Action
    displayName: Action
    type: string
    default: 'Plan'
    values:
    - Plan
    - Apply
    - Destroy

variables:
  - name: backendServiceArm
    value: 'thomasthorntoncloud'
  - name: backendAzureRmResourceGroupName
    value: 'thomasthorntoncloud'
  - name: backendAzureRmStorageAccountName
    value: 'thomasthorntontfstate'
  - name: backendAzureRmContainerName
    value: 'dockertoaci'
  - name: backendAzureRmKey
    value: 'terraform.tfstate'
  - name: backendAzureRmKeyAci
    value: 'terraform-aci.tfstate'
  - name: environment
    value: 'production'
  - name: terraform_version
    value: '1.0.10'
  - name: action
    value: ${{ parameters.Action }}
  - name: repository
    value: 'aci'
  - name: dockerfile
    value: '$(Build.SourcesDirectory)/aspnet-core-dotnet-core/Dockerfile'
  - name: containerRegistry
    value: 'tamopsciacr'

stages :   
  - stage: terraform_base
    condition: ne('${{ parameters.Action }}', 'Destroy')
    jobs:
      - template: templates/terraform-apply.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKey }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}

  - stage: Build
    dependsOn: [terraform_base]
    displayName: Build and push stage
    jobs:
    - template: templates/docker.yaml
      parameters:
        repository: ${{ variables.repository }}
        dockerfile: ${{ variables.dockerfile }}
        containerRegistry: ${{ variables.containerRegistry }}

  - stage: terraform__aci
    dependsOn: [Build]
    condition: ne('${{ parameters.Action }}', 'Destroy')
    jobs:
      - template: templates/terraform-apply.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKeyAci }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform-aci/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}

  - stage: terraform_destroy
    condition: contains('${{ parameters.Action }}', 'Destroy')
    jobs:
      - template: templates/terraform-destroy.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKeyAci }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform-aci/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}
          job_name: 'terraform_destroy_aci'

      - template: templates/terraform-destroy.yaml
        parameters:
          backendServiceArm: '${{ variables.backendServiceArm }}'
          backendAzureRmResourceGroupName: '${{ variables.backendAzureRmResourceGroupName }}'
          backendAzureRmStorageAccountName: '${{ variables.backendAzureRmStorageAccountName }}'
          backendAzureRmContainerName: '${{ variables.backendAzureRmContainerName }}'
          backendAzureRmKey: '${{ variables.backendAzureRmKey }}' 
          workingDirectory: '$(System.DefaultWorkingDirectory)/terraform/'
          environment: ${{ variables.environment }}
          terraform_version: ${{ variables.terraform_version }}
          job_name: 'terraform_destroy_base'

Deploy this full pipeline and it will deploy an Azure Container Instance with the Image created from the previous step

Reviewing in the Azure Portal – we can see the new Container Instance

Accessing the FQDN of the Container Instance – loads the sample Application

Configuring the pipeline for CI/CD

Lets start the CI/CD journey, once we commit a change to the source code – we want the Container Instance to have been deployed with the latest commit automatically.

Lets look at this further with the below diagram:

  1. Developers commits code change to Azure Repo
  2. Azure Pipeline trigger, triggers a build with merge to main branch happens
  3. Azure Pipeline flows throw the stages mentioned above
  4. Azure Pipeline Builds and pushes latest change in code to a new image within the Azure Container Registry
  5. Stage terraform_aci will pull the latest image from Azure Container Registry and deploy this to Azure Container Instance
  6. Back to the developer to commit next change and this process happens again

Add CI/CD steps to the Azure DevOps Pipeline

Update the pipeline trigger to run pipeline when code is merged into main branch

trigger:
  batch: true
  branches:
    include:
    - main

Both stages Build and push stage & terraform_aci already cater for CI/CD

Lets look at the build and push stage with the task Docker@2 – notice the tag? It will tag the image with the latest BuildId

      - task: Docker@2
        displayName: Build and push an image to container registry
        condition: and(succeeded(), eq(variables['Action'], 'Apply'))
        inputs:
          command: buildAndPush
          repository: ${{ parameters.repository }}
          dockerfile: ${{ parameters.dockerfile }}
          containerRegistry: ${{ parameters.containerRegistry }}
          tags: '$(Build.BuildId)'

The terraform_aci stage grabs the similar tag during the Azure Container Instance creation

    image  = "${data.azurerm_container_registry.acr.login_server}/aci:${var.build_id}"

Time to CI/CD!

I made a small change to the source code and merged into main branch – the pipeline runs automatically when merged

Once its ran through, checking the Azure Container Instance URL – I can see my change!

GitHub repository for example code used above

2 comments

Leave a Reply