Engineering Compliant Azure Shared Image Gallery Images and sharing them across multiple Azure Active Directory Tenants

The Azure Shared Image Gallery service is an excellent service for globally distributing images for use in Azure across your organization. There are several ways which you can use to build & publish these images to your Azure Shared Image Gallery.

Engineering Compliant Azure Shared Image Gallery Images and sharing them across multiple Azure Active Directory Tenants

The Azure Shared Image Gallery service is an excellent service for globally distributing images for use in Azure across your organization. There are several ways which you can use to build & publish these images to your Azure Shared Image Gallery.

  1. Azure Image Builder (PREVIEW) - the service is built on top of Packer and abstracts away some of the complexity required when having to automate the packer tools.
  2. Hashicorp Packer - We can also directly perform the build using the Packer CLI.

For the orchestration of the end to end build and publishing of the compliant image we can leverage Azure Pipelines or any similar CI/CD solution.

In this post I want to take a closer look at the scenario where an organization has also made the decision to leverage multiple Azure Active Directory Tenants, there are a couple of reasons organizations may want to do this.

The primary reason being that AAD is a flat directory which makes it harder to keep objects for different types of environments separate. It also makes a lot of sense to test AAD config changes before applying them directly to your production Tenant. A common setup may look like the diagram below, where both the Engineering and Test tenants would be used exclusively for organizations platform engineering tasks.

This setup translates into three different environments within the organization:

  • Engineering - This is where platform services are engineered & tested by a small group of platform engineers.
  • Test - Once the solution is engineered the changes are promoted to this environment to be tested, the testing team would have access to these environments.
  • Production - Once changes are fully engineered, tested, and approved they would be deployed to Production; this is also where the organizations product teams deploy their application workloads.

If we were to take a closer look at how this would map to a CI/CD solution like Azure Pipelines, it may look something like the following:

As you can see from the diagram in this Azure Muti-Stage Pipeline we have defined three Azure Pipeline stages, each stage maps to one of our environments. When deploying changes, each deployment is promoted from the lower to the higher environment. We are also able to implement controls which apply when promoting deployments between stages and ensure that they remain compliant.

When engineering a platform feature, the process may look something like the following:

  1. An Azure Boards WorkItem is assigned to the Engineer.
  2. The Engineer creates feature branch in the relevant GitHub Repository and links to it from the Azure Boards WorkItem.
  3. The Engineer commits his changes and creates a PR for master.
  4. One of his colleagues reviews the PR before accepting the request and merging changes to master.
  5. A CI trigger is configured for the GitHub Repository, once changes are merged to master a CI pipeline executes.
    • We enforce compliant builds leveraging Pipeline Templates and/or Pipeline Decorators.
      • Disable restricted tasks & commands.
      • Enforce jobs execute in a Container, leveraging a versioned Container Image.
      • Inject additional tasks like CredScan, BinSkim, WhiteSource etc.
  6. All artifacts are published to a trusted artifact repository (Docker Registry/Azure Artifacts/Shared Image Gallery etc) which is regularly scanned for compliance.
  7. CD Trigger is configured for our Artifact repository and kicks off the Multi-Stage pipeline with ENG being automatically deployed and the upper stages required Manual Approval.
  8. We enforce compliant releases leveraging Pipeline Templates and/or Pipeline Decorators.
    • Using approvals and checks we verify that the pipeline which is executing extends from a trusted Pipeline Template which enforces compliance rules. We can apply approvals and checks to:
      • Pipeline Environments
      • Agent pools
      • Service Connections i.e. AzureRM service Connection, in this case we have a subscription per deployment environment.
  9. A manual approval is required from cloud ops before the deployment proceeds.
  10. If required we can integrate with on-prem release management systems by posting a message onto a Azure Service Bus queue which allows us to communicate with otherwise disconnected system running on-prem like an internal SNOW instance. Once the message in processed Azure Pipelines can be notified of the Approval/Rejection of the release.

Ok so that's a bit of background, so how can we leverage what we learned above for the engineering of compliant OS Images. Let's look at each of the stages within our pipeline and which steps should be executed.

  1. Engineering
    • Build the image & run any tests i.e. Pester.
    • Publish the freshly built image to the Engineering Azure Shared Image Gallery.
  2. Test
    • Copy the Engineering Azure Shared Image Gallery Image to the Test Azure Shared Image Gallery.
    • Deploy a Virtual Machine running in our Test Environment using this version of the Image.
    • Run Functional Tests on this machine.
  3. Production
    • Copy the Test Azure Shared Image Gallery Image to the Production Azure Shared Image Gallery.

In this post we focus on the promotion of Azure Shared Image Gallery Images between stages i.e. environments, we will not look at the Packer build step in any detail. If we map our steps to the CI/CD process depicted in the diagram above it may look something like the following:

To enable us to copy our image we will need one Service Principal per stage which will be used to promote the image version between galleries in different subscriptions & tenants. The scenario illustrated above shows how we can achieve this by leveraging the Service Principal of the current promoted stage to reach back (current stage-1) to copy the versioned image which is being promoted i.e. ENG -> TEST -> PROD.

The following resource describes the steps that are required to configure our Service Principals with the required permissions, this was used as the basis for the steps outlined below.

  1. Engineering Stage

    • Single Tenant Application registration & Service Principal - which provides only contributor access to the Engineering Azure Shared Image Gallery.
      SingleTenantAAD-1
  2. Test Stage

    • Multi-tenant Application Registration.
      MultiTenantAAD
      • Service Principal Permissions.
        • Requires read access to the Engineering Azure Shared Image Gallery Resource Group.
          1. Authorize the application "https://login.microsoftonline.com/\/oauth2/authorize?client_id=<TEST Application (client) ID>&response_type=code&redirect_uri=https%3A%2F%2Fwww.microsoft.com%2F".
          2. Add Reader Role for the Engineering Azure Shared Image Gallery Resource Group.
            SIGIAM-2
        • Requires access to the Test Azure Shared Image Gallery.
          1. Add Contributor role on the Test Azure Shared Image Gallery Resource.
            SIGIAMC
  3. Production Stage

    • Multi-tenant Application Registration.
      MultiTenantAAD
      • Service Principal Permissions.
        • Requires read access to the Test Azure Shared Image Gallery Resource Group.
          1. Authorize the application "https://login.microsoftonline.com/\/oauth2/authorize?client_id=<PROD Application (client) ID>&response_type=code&redirect_uri=https%3A%2F%2Fwww.microsoft.com%2F".
          2. Add Reader Role for the Test Azure Shared Image Gallery Resource Group.
            SIGIAM-2
        • Requires access to the Production Azure Shared Image Gallery.
          1. Add Contributor role on the Production Azure Shared Image Gallery resource.
            SIGIAMC

Once permissions are correctly configured, we can leverage these Service Principals to Copy images from another gallery using the Azure CLI. The ability to copy Azure Shared Image Gallery Images across tenants was added in the Azure CLI 2.9.0, therefore please make sure you are using this version or a newer version of the CLI.

Let's look at the basic setup of our pipeline, you will need the following pre-requisites:

  • Azure DevOps Service Connection per Service Principal created above.
  • Install the Packer Azure Pipelines extension, after successful build this task will set an Azure Pipeline output variable called ManagedImageSharedImageGalleryId.
  • Setup the required Azure Pipeline variables or Azure Pipeline Variable Groups.

The resulting stages YAML for our Multi-stage pipeline would look something like the following:

stages:
- stage: ENG
  jobs:
  - job: ENG_BUILD_IMAGE
    steps:
    - task: PackerTool@0
      displayName: 'Use Packer 1.6.4'
      inputs:
        version: '1.6.4'
    - task: Packer@1
      displayName: 'Build image' # Output variable is automatically set for 'ManagedImageSharedImageGalleryId'
      name: buildImage
- stage: TEST
  dependsOn:
  - ENG
  jobs:
  - job: TEST_COPY_IMAGE
    steps:
    - task: AzureCLI@2
      displayName: Get Gallery Image Version Id
      name: GetGalleryImageVersionId
      inputs:
        azureSubscription: 'test-arm-subscription'
        scriptType: 'ps'
        scriptLocation: 'inlineScript'
        inlineScript: |
          # Login using both the Source & Destination Service Principal
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:source_gallery_tenant_id
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:tenantId
          # Set Current Subscription to Destination Subscription
          az account set --subscription $env:destination_gallery_subscription_id
          
          # Get the id for the copied image version          
          $ImageVersionId=az sig image-version show `
          --resource-group $env:destination_image_resource_group `
          --gallery-name $env:destination_gallery_name `
          --gallery-image-definition $env:image_prefix `
          --gallery-image-version $env:image_version `
          --query "id" -o tsv
          
          Write-Host "##vso[task.setvariable variable=ManagedImageSharedImageGalleryId;isOutput=true]$ImageVersionId"
        addSpnToEnvironment: true    
    - task: AzureCLI@2
      displayName: Copy Image
      inputs:
        azureSubscription: 'test-arm-subscription'
        scriptType: 'ps'
        scriptLocation: 'inlineScript'
        inlineScript: |
          # Login using both the Source & Destination Service Principal
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:source_gallery_tenant_id
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:tenantId
          # Set Current Subscription to Destination Subscription
          az account set --subscription $env:destination_gallery_subscription_id

          # Copy the Image Version, Image must already exist in both locations          
          az sig image-version create `
          --resource-group $env:destination_image_resource_group `
          --gallery-name $env:destination_image_gallery `
          --gallery-image-definition $env:image_prefix `
          --gallery-image-version $env:image_version `
          --target-regions westeurope `
          --replica-count 1 `
          --managed-image $env:GetGalleryImageVersionId_ManagedImageSharedImageGalleryId
        addSpnToEnvironment: true
- stage: PRDO
  dependsOn:
  - TEST
  jobs:
  - job: PROD_COPY_IMAGE
    steps:  
    - task: AzureCLI@2
      displayName: Get Gallery Image Version Id
      name: GetGalleryImageVersionId
      inputs:
        azureSubscription: 'prod-arm-subscription'
        scriptType: 'ps'
        scriptLocation: 'inlineScript'
        inlineScript: |
          # Login using both the Source & Destination Service Principal
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:source_gallery_tenant_id
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:tenantId
          # Set Current Subscription to Destination Subscription
          az account set --subscription $env:destination_gallery_subscription_id
          
          # Get the id for the copied image version          
          $ImageVersionId=az sig image-version show `
          --resource-group $env:destination_image_resource_group `
          --gallery-name $env:destination_gallery_name `
          --gallery-image-definition $env:image_prefix `
          --gallery-image-version $env:image_version `
          --query "id" -o tsv
          
          Write-Host "##vso[task.setvariable variable=ManagedImageSharedImageGalleryId;isOutput=true]$ImageVersionId"
        addSpnToEnvironment: true    
    - task: AzureCLI@2
      displayName: Copy Image
      inputs:
        azureSubscription: 'prod-arm-subscription'
        scriptType: 'ps'
        scriptLocation: 'inlineScript'
        inlineScript: |
          # Login using both the Source & Destination Service Principal
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:source_gallery_tenant_id
          az login --service-principal -u $env:servicePrincipalId -p $env:servicePrincipalKey --tenant $env:tenantId
          # Set Current Subscription to Destination Subscription
          az account set --subscription $env:destination_gallery_subscription_id

          # Copy the Image Version, Image must already exist in both locations          
          az sig image-version create `
          --resource-group $env:destination_image_resource_group `
          --gallery-name $env:destination_image_gallery `
          --gallery-image-definition $env:image_prefix `
          --gallery-image-version $env:image_version `
          --target-regions westeurope `
          --replica-count 1 `
          --managed-image $env:GetGalleryImageVersionId_ManagedImageSharedImageGalleryId
        addSpnToEnvironment: true

I hope you enjoyed reading this post and that you found the information useful.