In the Using Azure Container Instance with multiple containers I introduced the use of YAML or JSON based ARM file to deploy multiple containers in an Azure Container Instance Group. Let’s see how we can automate this deployment in a continuous deployment chain.
In this example I will use an application based on a web front-end and a backend API. To deploy this kind of service, you will have to create a Dockerfile for both front-end and backend, push the two images in a registry, create a YAML or ARM Json file and deploy the Azure Container Instance group.
But how can you update containers images and/or containers settings after the initial deployment?
Fortunately the two deployment methods, az container create and New-AzResourceGroupDeployment will only update resources if there is a difference between the YAML or the JSON file and what is deployed on Azure.
To automate the process, I will use Azure DevOps with a YAML pipeline.
There are two important tasks:
- The first one is to build Docker images and push them to a registry
- The second one is to deploy the container group to Azure.
Before writing your azure-pipeline.yml you need to create a services connection. Services connections in Azure DevOps allow you to connect to a service like a docker registry, Azure resource by using an identity managed by the pipeline. Using a service connection prevents you to store sensitive information like service keys and passwords in your repository.
For Azure Resource Manager, you will need to create a Service Principal, an identity to access your resource in Azure.
The service principal will be used to grant access to the resource group where you plan to deploy the Container Instance group object.
$ResourceGroupName = "omc-lab-aci"
$ServicePrincipal = New-AzADServicePrincipal -DisplayName "ACI-demo-sp" -SkipAssignment
New-AzRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $ServicePrincipal.ApplicationId -ResourceGroup $ResourceGroupName
Now you need to extract the service principal APP ID, the Secret, and the Tenant ID
$SecureStringBinary = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($servicePrincipal.Secret)
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto($SecureStringBinary)
To create the service connection in your Azure DevOps project. Open Project settings, go to Pipelines, and click on Service connections.
We need two service connections, one docker registry, and one Azure Resource Manager.
For Azure Resource Manager, select Service Principal
Select Subscription for the scope level, enter the Subscription ID and name, the App ID in the Service Principal Id field, the service principal key, and the TenantId.
Click on verify and save
For the Azure Container Registry, repeat the process but you will need to choose Docker Registry instead of Azure Resource Manager.
Select Azure Container Registry, your Subscription, and your registry
The wizard creates the service principal and assigns the contributor role on your registry.
Now that you have the two service connections you can start to create your pipeline.
At the root of your repository, create an azure-pipeline.yml.
The pipeline needs to start each time we update the master branch using the Azure DevOps repository.
trigger:
- master
resources:
- repo: self
To facilitate your work you can create some variables.
variables:
dockerRegistryServiceConnection: 'omcdockerreg01'
azureARMServiceConnection: 'ACR-Connection'
imageRepository: 'demoaci'
containerRegistry: 'myacr.azurecr.io'
aciManifestTemplateFile: '$(Build.SourcesDirectory)/deploy-aci.yaml'
aciManifestFile: '$(Build.SourcesDirectory)/deploy.yaml'
resourceGroupName: 'myresourcegroup'
tag: '$(Build.BuildId)'
The two service connections, the docker registry root, and URI, the resource group to deploy to. The template manifest file to be used during the deployment create a deploy.yaml (aciManifestFile) file using the tag value.
You may need a password for Azure Container Registry. It will be a really bad idea to store it manifest template file. Instead you can use a secret variable to your pipeline. You need to open Azure DevOps, go to Pipelines, and then select Pipelines. If there is no pipeline you will need to save your azure-pipelines.yml file, commit your work, and push to Azure DevOps. You will see your pipeline after that. Select your pipeline and then click on the edit button on the upper right side. Click on variables and add a new secret variable named acrSecret.
You need to create the manifest template file.
apiVersion: 2018-10-01
location: westeurope
name: azuredemoaci
type: Microsoft.ContainerInstance/containerGroups
properties:
osType: Linux
ipAddress:
type: Public
dnsNameLabel: azuredemoaci-omc01
ports:
- protocol: tcp
port: '8000'
imageRegistryCredentials:
- server: myacr.azurecr.io
username: Username
password: %ACRPASS%
restartPolicy: Always
containers:
- name: omcdemoaci-front
properties:
image: myacr.azurecr.io/demoaci/omcdemoaci-front:%TAG%
command: ['pwsh', '/entrypoint.ps1']
ports:
- port: 8000
resources:
requests:
cpu: 1.0
memoryInGB: 1.0
- name: omcdemoaci-api
properties:
image: myacr.azurecr.io/demoaci/omcdemoaci-api:%TAG%
command: ['pwsh', '/entrypoint.ps1']
resources:
requests:
cpu: 1.0
memoryInGB: 1.0
Notice the registry password has been replaced by %ACRPASS% as well the tag for the image with %TAG%
To create your final deployment file, you will need a script to replace these values. I made a simple PowerShell script to perform this task.
[cmdletbinding()]
param (
[Parameter(Mandatory=$false)]
[String]
$TemplateManifestFilePath=[Environment]::GetEnvironmentVariable('ACIMANIFESTTEMPLATEFILE'),
[Parameter(Mandatory=$false)]
[String]
$ManifestFilePath=[Environment]::GetEnvironmentVariable('ACIMANIFESTFILE'),
[Parameter(Mandatory=$false)]
[String]
$BuildTag=[Environment]::GetEnvironmentVariable('TAG'),
[Parameter(Mandatory=$false)]
[String]
$AcrSecret=[Environment]::GetEnvironmentVariable('ACRSECRET')
)
try {
if (!(test-path -Path $TemplateManifestFilePath -ErrorAction SilentlyContinue)) {
Throw "NO Manifest Template File, please add a Manifest Template"
Exit
}
if (test-path -Path $ManifestFilePath -ErrorAction SilentlyContinue) {
Remove-Item -Path $ManifestFilePath -force
}
$Yaml = (Get-Content -path $TemplateManifestFilePath -Raw) -replace "%TAG%",$BuildTag
$Yaml = $Yaml -replace "%ACRPASS%",$AcrSecret
new-item -Path $ManifestFilePath -ItemType File
$Yaml | Add-Content -Path $ManifestFilePath -Encoding utf8
}
catch {
Write-Error -Message " Exception Type: $($_.Exception.GetType().FullName) $($_.Exception.Message)"
}
Pipeline variables can be retrieved in your PowerShell script as environment variables (except for secret variables, you need an extra step to be able to get them in your scripts).
You can no add the two stages of your deployment pipeline, build and push docker images, and deploy the container instance group.
For the first stage
vmImageName: 'ubuntu-latest'
stages:
- stage: BuildDockerImage
displayName: Build Docker Images and push stage
jobs:
- job: BuildApi
displayName: BuildApi
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)/omcdemoaci-api
dockerfile: '$(Build.SourcesDirectory)/API/Dockerfile'
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
- job: BuildFront
displayName: BuildFront
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)/omcdemoaci-front
dockerfile: '$(Build.SourcesDirectory)/FRONT/Dockerfile'
containerRegistry: $(dockerRegistryServiceConnection)
tags: |
$(tag)
And for the second
- stage: BuildManifestAndDeploy
displayName: Build Deploy Manifest with docker tag and deploy to Azure
jobs:
- job: BuildAndDeploy
displayName: BuildApi
pool:
vmImage: $(vmImageName)
steps:
- task: PowerShell@2
env:
ACRSECRET: $(acrSecret)
displayName: Create the deployment Manifest
inputs:
targetType: 'filePath'
filePath: '$(System.DefaultWorkingDirectory)/buildmanifest.ps1'
- task: AzureCLI@2
displayName: 'Deploy to Azure'
inputs:
azureSubscription: $(azureARMServiceConnection)
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: az container create --resource-group $(resourceGroupName) --file $(aciManifestFile)
Note, to retrieve secret variable in the PowerShell task, you must explicitly add it as an environment variable.
env:
ACRSECRET: $(acrSecret)
Once the file is committed and pushed to Azure DevOps, the pipeline should work and start building and create or update the Azure Container Instance group.
Each time the pipeline run, it will update the docker images tag in the instance based on the build number. You can now concentre yourself on what is valuable, your application.
Top comments (1)
Thank you! I have some questions
In case you have the Front and the Backend in different codebases (different repos), how you could trigger the build pipeline?
let's say you built a trigger to start the pipeline, how to prevent the other container from being restarted (how to only deploy the issuer container)?