Azure Container Instance Group Continuous deployment
Tags: azure, docker, devops, cicd
Published: 2020-05-24 21:11:06

In the [Using Azure Container Instance with multiple containers]( https://dev.to/omiossec/using-azure-container-instance-with-multiple-containers-43fd) 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. 

```powershell
$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

```powershell
$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.  

```yaml
trigger:
- master

resources:
- repo: self
```

To facilitate your work you can create some variables.

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

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

```powershell
[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 

```yaml

  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

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

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