On-Demand macOS VMs in Azure DevOps Pipelines with Anka

If you run iOS or macOS builds in Azure DevOps Pipelines, Microsoft-hosted agents only go so far. Self-hosted agents on Mac hardware work, but a long-lived Mac agent drifts, shares state between jobs, and is hard to scale cleanly. This post shows how to run macOS VMs in Azure DevOps with Anka Build: clone a template per pipeline run, execute your job inside the VM, publish artifacts, then delete the VM.

Anka Build creates and manages macOS VMs on Mac hosts in a container-like loop. You build VM templates, store them in Anka Registry, and start instances on local or cloud Macs (including AWS EC2 Mac). Multiple VMs can share one Mac, so one agent host can cover more concurrent pipeline jobs.

Anklet and Azure DevOps: what Microsoft blocks

For GitHub Actions, Anklet is the orchestrator that listens for jobs, starts an Anka VM, registers a runner, and cleans up when the job finishes. Developers keep normal workflow YAML; infra owns the fleet. See Enterprise macOS GitHub Actions Runners with Anka.

We want the same model for Azure DevOps. An Azure plugin is tracked in anklet#50, but standard self-hosted agent pools do not support true provision-on-demand. If no registered agent matches the job’s pool and demands at queue time, Azure rejects the job immediately instead of waiting for Anklet (or any provisioner) to start a VM and register an agent. Microsoft confirmed that waiting only happens when a matching agent already exists in the pool. Details and the support reply are in this anklet#50 comment.

If you need that behavior from Azure, upvote Microsoft’s request: Keep jobs waiting even if there are no agents that match demands. Until that changes, the reliable pattern is the one below: keep at least one self-hosted agent online in the pool, and have that agent clone an Anka VM for each pipeline run.

What you need

One Mac with Anka installed, at least one VM template/tag ready on that host, and an Azure DevOps project in your organization. Microsoft’s overview of Azure DevOps is here.

From Microsoft’s What is Azure Pipelines? docs:

“Azure Pipelines automatically builds and tests code projects to make them available to others. It works with just about any language or project type. Azure Pipelines combines continuous integration (CI) and continuous delivery (CD) to test and build your code and ship it to any target.”

Pipelines are YAML-driven, similar to GitHub Actions. Below is an example that prepares an Anka VM, runs Fastlane inside it, publishes results, and always cleans up. The agent in the pool is already registered; the “on-demand” part is the Anka VM, not the Azure agent itself.

trigger:
  - main

pool: 'Anka macOS'

parameters:
  - name: anka_vm_name
    displayName: "Anka VM Template Name or UUID"
    type: string
    default: '11.5.2'
  - name: anka_vm_tag_name
    displayName: "Anka VM Tag Name"
    type: string
    default: 'vanilla+port-forward-22+brew-git'
  - name: lane_name
    displayName: "Fastlane Lane Name"
    type: string
    default: ''
  - name: lane_parameters
    displayName: "Fastlane Parameters"
    type: string
    default: ''
  - name: publishFolder
    displayName: "Artifact Publish Folder"
    type: string
    default: ''
  - name: artifactFolderName
    displayName: "Artifact Folder Name"
    type: string
    default: 'Artifacts'
  - name: publishTest
    displayName: "Publish Test Path"
    type: string
    default: ''
  - name: publishCodeCoverageFolderName
    displayName: "Publish Code Coverage Folder"
    type: string
    default: ''
  - name: rubyVersion
    displayName: "Ruby Version"
    type: string
    default: 'ruby-2.7.0'
  - name: match_pass_key
    displayName: "Match Password"
    type: string
    default: 'Password Here'
  - name: git_token_key
    displayName: "Git Token"
    type: string
    default: 'Token Here'

steps:

  - task: Bash@3
    displayName: 'Create Anka VM'
    inputs:
      targetType: 'inline'
      script: |
        # Pull one template at a time on a node to avoid registry corruption.
        while [[ -f "/tmp/registry-pull-lock-${{ parameters.anka_vm_name }}" ]]; do
          echo "Lock file found... Another job on this node is pulling a tag for ${{ parameters.anka_vm_name }}. Sleeping for 20 seconds..."
          sleep 20
        done

        touch "/tmp/registry-pull-lock-${{ parameters.anka_vm_name }}"

        # Optional: pull latest from Anka Registry before clone
        # anka registry pull ${{ parameters.anka_vm_name }} -t ${{ parameters.anka_vm_tag_name }}

        anka clone ${{ parameters.anka_vm_name }} ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name)

  - task: Bash@3
    displayName: 'Unlock Anka VM pull'
    condition: always()
    inputs:
      targetType: 'inline'
      script: |
        rm -f "/tmp/registry-pull-lock-${{ parameters.anka_vm_name }}"

  - task: Bash@3
    displayName: 'Prepare Anka VM working directory'
    inputs:
      targetType: 'inline'
      script: |
        anka start ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name)
        anka cp -fa ./ ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name):./work/

  - task: Bash@3
    displayName: 'Run fastlane in Anka VM'
    inputs:
      targetType: 'inline'
      script: |
        anka run --env --no-volume --wait-network --wait-time ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name) bash -c "cd work
        bundle install
        bundle exec fastlane ${{ parameters.lane_name }} ${{ parameters.lane_parameters }}"

  - task: Bash@3
    displayName: "Copy results from Anka VM"
    inputs:
      targetType: 'inline'
      script: |
        anka cp -fa ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name):work/ $(Build.ArtifactStagingDirectory)/../s/vm_result/

  - ${{ if ne(parameters.publishFolder, '') }}:

    - task: PublishBuildArtifacts@1
      displayName: 'Publish artifacts'
      inputs:
        pathToPublish: '$(Build.ArtifactStagingDirectory)/../s/vm_result/${{ parameters.publishFolder }}'
        artifactFolderName: '${{ parameters.artifactFolderName }}'

  - ${{ if ne(parameters.publishTest, '') }}:

    - task: PublishTestResults@2
      displayName: 'Upload test results'
      inputs:
        testResultsFormat: 'JUnit'
        testResultsFiles: '$(Build.ArtifactStagingDirectory)/../s/vm_result/${{ parameters.publishTest }}'
        testRunTitle: 'Unit Tests'

  - ${{ if ne(parameters.publishCodeCoverageFolderName, '') }}:

    - task: UseDotNet@2
      displayName: 'Setting up Code Coverage'
      inputs:
        version: '5.0.x'
    - task: publishCodeCoverageFolderNameResults@1
      displayName: 'Upload code coverage results'
      inputs:
        codeCoverageTool: 'Cobertura'
        summaryFileLocation: '$(Build.ArtifactStagingDirectory)/../s/vm_result/${{ parameters.publishCodeCoverageFolderName }}/xml/cobertura.xml'

  - task: Bash@3
    displayName: "Cleanup Anka VM"
    condition: always()
    inputs:
      targetType: 'inline'
      script: |
        anka delete --yes ado-fastlane+$(Build.Repository.Name)_$(Build.SourceBranchName)_$(Build.SourceVersion)_$(Build.BuildNumber)_$(Agent.Name)

Register a self-hosted agent, then run the YAML

The maintained copy lives in our Azure DevOps examples repo. The pool value must match a self-hosted agent pool that already has at least one online agent. Download the latest agent from microsoft/azure-pipelines-agent, unpack it on the Mac, and register it to a pool such as Anka macOS using Microsoft’s macOS agent docs.

After the agent is Idle in that pool, run the YAML. Override parameters at queue time when you need a different template, tag, or Fastlane lane:

Azure DevOps pipeline parameters for Anka VM template and Fastlane lane

How the job uses Anka

The early steps clone from a VM template into a unique instance name. Treat the template as read-only so later runs do not inherit leftover packages or credentials.

Azure Pipelines clones your repo onto the agent host. The pipeline then copies that working tree into the VM with anka cp, runs commands via anka run, and copies results back for Publish tasks.

Azure DevOps pipeline running Fastlane inside an Anka VM

Always delete the VM with condition: always() so failed jobs do not leave orphans on the host.

Because the pipeline YAML lives in the repo, app teams can adjust build and test steps without waiting on a custom Azure task. Developers can also validate steps locally with Anka Develop before pushing.

Next steps

Share this post

AWS + Anka Build Cost Diagramv3
Ephemeral macOS VMs on AWS EC2 Mac with Anka
Run ephemeral macOS VMs on AWS EC2 Mac with Anka and Anklet. Pack more iOS CI capacity per instance, start jobs in seconds, and cut cost.
Read More
Screenshot 2025-01-08 at 2.16
Enterprise macOS GitHub Actions Runners with Anka
Run self-hosted macOS GitHub Actions at enterprise scale with Anka and Anklet: ephemeral Apple Silicon VMs, more control than hosted runners.
Read More
The Anka product ladder: Develop, Flow, Build, and EC2 Mac as four ascending steps, with Crypt, MCP, Anka Scan, and AMI Scan named below
Which Anka Product Do You Actually Need? A Walkthrough of the Whole Lineup
A situation-first guide to every Veertu product: Anka Develop, Anka Flow, Anka Build, AWS EC2 Mac, Anka Crypt, Anka MCP, Anka Scan, and EC2 Mac AMI Scan, including the moment you move from one to the next.
Read More
anka2024v1-1536x768
A Year of Anka: Highlights from 2024
We’re starting a new annual tradition here at Veertu with our A Year of Anka blog posts. We want our customers to know how the product has grown over the past year and think this is a great avenue to do so. Please enjoy and happy holidays from all...
Read More
anka-or-1
Anka vs Orka in 2024
It has been several years since we made our first side by side comparison between Anka and Orka. A lot has changed, and we believe it’s important to make sure the information out there is accurate. We’ll be specifically addressing a newer...
Read More
networking-performancev1
Unlocking Superior macOS VM Network Performance: Introducing Anka's new networking mode for Apple Silicon
Large and complex enterprises using Anka have many different demands, and we have worked to continue to develop innovative technology to meet these demands. Enterprise infrastructure hardware is often on the cutting edge, and they need advanced capabilities...
Read More
gitlab-with-anka
Anka Cloud Gitlab Executor
Veertu’s Anka and the new Anka Cloud Gitlab Executor Veertu’s Anka is a suite of software tools built on the macOS virtualization platform. It enables the execution of single or multi-use macOS virtual machines (VMs) in a manner similar to Docker....
Read More
mac-scan-v1
Real-Time CVE Scanning of your macOS Build Systems
It’s common that an organization’s macOS build system will download thousands, sometimes tens of thousands of third-party dependencies every hour. When building and testing iOS applications, it typically downloads and installs third-party...
Read More
anka-on-silicon-v1
The ONLY Fully Automated Apple Silicon macOS VM Creation Solution
Starting in Anka 3.1 we announced that Anka is now able to fully automate the macOS installation processes, disabling SIP, and enabling VNC — all previously manual steps users had to perform inside o the VM. At the time of writing this article,...
Read More
anka_click
Scripting macOS UI User Actions With Anka Click
Starting in Anka 3.2, we’ve introduced a solution for scripting macOS UI user actions. You may ask, “Why would I want to do that?”. Well, often macOS configuration and applications do not have a CLI allowing you to perform certain actions...
Read More