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 Registrytamopsciacr
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
aspnet-core-dotnet-core
will store the application code andDockerfile
to build the image.- Notice the
templates
folder? Rather than duplicating myself within the Azure DevOps pipeline – I have setup templates that I can reference multiple times without copy/pasting each time. This blog post Creating templates in Azure DevOps Pipelines details further templating! terraform
&terraform-aci
folders – the first folder contains theterraform_base
stage Terraform configuration &terraform-aci
contains the Terraform to deploy Azure Container Instancetfvars
stores tfvars
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


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:

- Developers commits code change to Azure Repo
- Azure Pipeline trigger, triggers a build with merge to main branch happens
- Azure Pipeline flows throw the stages mentioned above
- Azure Pipeline Builds and pushes latest change in code to a new image within the Azure Container Registry
- Stage
terraform_aci
will pull the latest image from Azure Container Registry and deploy this to Azure Container Instance - 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!

2 comments