Search code examples
dockerazure-devopsdockerfileazure-pipelines-yaml

In Azure pipeline, intermediate docker images generated in previous steps within the same run weren't retained when running on Microsoft-hosted agent


[Edit on Sep 20, 2023] The issue was found within the same pipeline run. It's not caused by Microsoft-hosted agent not retaining any data between different runs.

[Original Question] We have an Azure DevOps yaml pipeline running on a self-hosted agent, it works fine. When we switch to use the Microsoft-hosted agent, we got an issue retrieving the intermediate unit test docker image generated in the previous build stage.

Dockerfile

# Phase 1: Build the source code
FROM nginx:latest AS build
WORKDIR /src
RUN echo "Build completed" > build.txt

# Phase 2: Run unit tests
FROM build as testrunner
WORKDIR /testresults
RUN echo "Placeholder" > placeholder.txt
RUN echo "Unit test completed" > testresults.txt
LABEL unittestlayer=true

# Phase 3: Publish the binary
FROM build as publish
WORKDIR /publish
COPY --from=build /src/build.txt .
RUN echo "Binary publish completed" > publish.txt

# Phase 4: Build the final docker image
FROM nginx:latest AS base
WORKDIR /app
COPY --from=publish /publish .
COPY --from=testrunner /testresults/placeholder.txt .

azure-pipelines.yml

trigger:
  - main

pool:
  vmImage: "ubuntu-22.04"

steps:
  - task: DockerCompose@0
    displayName: "Build and Test"
    inputs:
      dockerComposeFile: "**/docker-compose.yml"
      dockerComposeFileArgs: |
        TAG=$(Build.BuildNumber)
      projectName: "demo"
      action: "Build services"

  - script: |
      docker images -a
      export unittestlayerid=$(docker images --filter "label=unittestlayer=true" -q)
      docker create --name unittestcontainer $unittestlayerid
      docker cp unittestcontainer:/testresults/ $(Build.ArtifactStagingDirectory)/testresults/
      docker stop unittestcontainer
      docker rm unittestcontainer
    displayName: "Retrieve Test and Coverage Results"
    continueOnError: false

The script in the pipeline failed to get the unit test image, docker images --filter "label=unittestlayer=true" -q returned empty result. There is also no intermediate docker images in the output of docker images -a command.

We prepared the minimal example code to replicate the issue and uploaded it to a public Azure DevOps project https://dev.azure.com/liangzugeng/_git/DockerBuildPipelineRepro, and the previous failed pipeline run contains the build output https://dev.azure.com/liangzugeng/DockerBuildPipelineRepro/_build/results?buildId=59&view=logs&j=12f1170f-54f2-53f3-20dd-22fc7dff55f9

What does Microsoft-hosted agent do differently to cause the issue and how can the intermediate docker image be retrieved in latter build steps within the same build pipeline?


Solution

  • The cause of the issue was that the docker BuildKit engine only exports the final image, it removes all intermediate images. This is a by-design behavior.

    When we migrated to the Microsoft-hosted agent, the docker build engine was changed from the classical one we had on our private agent to the new BuildKit engine. Which caused the issue.

    See GitHub issues below, our solution was to build needed intermediate images in separate steps and tag them, then use the tagged intermediate images in the following build steps.

    1. https://github.com/docker/for-mac/issues/6988
    2. https://github.com/actions/runner-images/issues/8350

    Revised azure pipeline:

    trigger:
      - main
    
    pool:
      vmImage: "ubuntu-22.04"
    
    steps:
      - script: |
          docker build -t dockerissue.repro:build --target build .
          docker build -t dockerissue.repro:testrunner --target testrunner .
          docker build -t dockerissue.repro:$(Build.BuildNumber) .
        displayName: "Build services"
        continueOnError: false
    
      - script: |
          docker images -a
          export unittestlayerid=$(docker images --filter "label=unittestlayer=true" -q)
          docker create --name unittestcontainer $unittestlayerid
          docker cp unittestcontainer:/testresults/ $(Build.ArtifactStagingDirectory)/testresults/
          docker stop unittestcontainer
          docker rm unittestcontainer
        displayName: "Retrieve Test and Coverage Results"
        continueOnError: false