Showing posts with label Azure DevOps. Show all posts
Showing posts with label Azure DevOps. Show all posts

Monday, August 24, 2026

Azure DevOps: Copy Azure Container Registry (ACR) Images to an External Tenant's ACR

Consider a scenario where an App Service running in an external tenant needs to pull an image from our own ACR. The lazy way is to share our ACR credentials (username/password) with the external party, so they can set DOCKER_REGISTRY_SERVER_USERNAME and DOCKER_REGISTRY_SERVER_PASSWORD.

But obviously that's not something we should be doing. We can't be handing our own passwords to anyone, and we shouldn't be using passwords or secrets in the first place.

You might think the fix is for them to give their App Service a managed identity and grant it AcrPull on our ACR. That doesn't work across tenants. A managed identity only exists in its own tenant.

What does work is copying the image into their ACR first. Then their App Service pulls from their own registry, in their own tenant, with their own managed identity, and that's just the ordinary same-tenant case.

So in this post, let's see how we can copy Azure Container Registry (ACR) images to an external tenant's ACR in Azure DevOps..

Prerequisites
  • Both registries need public network access. A registry on Selected networks still counts as enabled, but it also has to allow trusted Azure services to bypass the network, which is on by default.
  • Both registries accept Entra ARM tokens. You can test it using the following script.
$TENANT_ID = "<TENANT_ID>"
$SUBSCRIPTION_ID = "<SUBSCRIPTION_ID>"

$ACR_RESOURCE_GROUP_NAME = "<ACR_RESOURCE_GROUP_NAME>"
$ACR_NAME = "<ACR_NAME>"
$ACR_RESOURCE_ID = "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$ACR_RESOURCE_GROUP_NAME/providers/Microsoft.ContainerRegistry/registries/$ACR_NAME"

# Login to tenant and set the subscription
az login `
  --tenant $TENANT_ID

az account set `
  --subscription $SUBSCRIPTION_ID

# Confirm the registry accepts Entra ARM tokens
az rest `
  --method get `
  --url "https://management.azure.com$ACR_RESOURCE_ID`?api-version=2023-01-01-preview" `
  --query "properties.policies.azureADAuthenticationAsArmPolicy.status" `
  --output tsv

The concept

  • Two Azure DevOps service connections, both in our project, each federated to a different managed identity in a different tenant. No secret in either one, just a trust.
  • Ours/Source: A user-assigned managed identity with AcrPull on our ACR, and a service connection federated to it. We add the federated credential to that identity ourselves, because it lives in our tenant.
    • If your source registry is ABAC-enabled, grant Container Registry Repository Reader instead. AcrPull isn't honoured there.
  • Theirs/External:  A user-assigned managed identity with Container Registry Data Importer and Data Reader on their ACR. We create the second service connection pointing at their tenant, and send them the Issuer and Subject it generates. They add the federated credential to their identity.
  • At deploy time the pipeline mints a short-lived token using the first connection, then calls az acr import with the second connection.

az acr import `
  --name "<THEIR_ACR_NAME>" `
  --resource-group "<THEIR_ACR_RESOURCE_GROUP_NAME>" `
  --source "<OUR_ACR_LOGIN_SERVER>/<IMAGE_NAME>:<IMAGE_TAG>" `
  --image "<IMAGE_NAME>:<IMAGE_TAG>" `
  --password "<ACCESS_TOKEN_CREATED_USING_OUR_SERVICE_CONNECTION>"

    • Note there is no --username. The access token is only accepted as a lone --password, and adding a username turns it into a basic auth pair.
    • az acr import authenticates the source and the target separately. 
    • The call goes to their registry as an identity in their tenant, and our registry is named in the same request with its own credential alongside. 
    • Their registry then pulls the image straight from ours, server to server, so the build agent never downloads it.

Now let's see this in action.

Some variables.

$SOURCE_TENANT_ID = "<OUR_TENANT_ID>"
$SOURCE_SUBSCRIPTION_ID = "<OUR_SUBSCRIPTION_ID>"

# Identity
$MIRROR_IDENTITY_NAME = "<MANAGED_IDENTITY_NAME>"
$MIRROR_IDENTITY_RESOURCE_GROUP_NAME = "<MANAGED_IDENTITY_RESOURCE_GROUP_NAME>"
$MIRROR_IDENTITY_LOCATION = "<LOCATION>"

# Source ACR
$SOURCE_ACR_RESOURCE_GROUP_NAME = "<ACR_RESOURCE_GROUP>"
$SOURCE_ACR_NAME = "<ACR_NAME>"
$SOURCE_ACR_RESOURCE_ID = "/subscriptions/$SOURCE_SUBSCRIPTION_ID/resourceGroups/$SOURCE_ACR_RESOURCE_GROUP_NAME/providers/Microsoft.ContainerRegistry/registries/$SOURCE_ACR_NAME"

First step is creating a Managed Identity.

# Login to the tenant and set subscription
az login `
  --tenant $SOURCE_TENANT_ID

az account set `
  --subscription $SOURCE_SUBSCRIPTION_ID

# Create a user-assigned managed identity
az identity create `
  --name $MIRROR_IDENTITY_NAME `
  --resource-group $MIRROR_IDENTITY_RESOURCE_GROUP_NAME `
  --subscription $SOURCE_SUBSCRIPTION_ID `
  --location $MIRROR_IDENTITY_LOCATION

$sourceIdentity = az identity show `
  --name $MIRROR_IDENTITY_NAME `
  --resource-group $MIRROR_IDENTITY_RESOURCE_GROUP_NAME `
  --subscription $SOURCE_SUBSCRIPTION_ID | ConvertFrom-Json

$MIRROR_IDENTITY_PRINCIPAL_ID = $sourceIdentity.principalId
$MIRROR_IDENTITY_CLIENT_ID = $sourceIdentity.clientId

Now let's grant the MI AcrPull OR Container Registry Repository Reader on our ACR.

# Assign the AcrPull OR Container Registry Repository Reader role to the managed identity
az role assignment create `
  --assignee-object-id $MIRROR_IDENTITY_PRINCIPAL_ID `
  --assignee-principal-type ServicePrincipal `
  --role "<AcrPull OR Container Registry Repository Reader>" `
  --scope $SOURCE_ACR_RESOURCE_ID

Now let's create the source Service Connection.

New Service Connection: Azure Resource Manager

Note: 

  • Identity Type: App registration or managed identity (manual)
  • Credential: Workload identity federation
  • Directory (tenant) ID: Our Tenant ID.

New Service Connection App Registration Details
Here, the Application (client) ID is the MIRROR_IDENTITY_CLIENT_ID.

Now before clicking on Verify and save, run the following using displayed Issuer and Subject identifier to create federated credentials.

# Create federated credential for the managed identity
az identity federated-credential create `
  --name azure-devops-mirror-source `
  --identity-name $MIRROR_IDENTITY_NAME `
  --resource-group $MIRROR_IDENTITY_RESOURCE_GROUP_NAME `
  --subscription $SOURCE_SUBSCRIPTION_ID `
  --issuer "<Issuer>" `
  --subject "<Subject identifier>" `
  --audiences "api://AzureADTokenExchange"
After this is executed, wait for a few seconds and then click on Verify and save. It should be verified and saved successfully.

Now we need to configure the external side. Almost all of the steps are exact same as above with different values. On external side,
  • The managed identity needs to be granted role: Container Registry Data Importer and Data Reader on their ACR.
  • To create the second service connection, the only thing we need from them up front is their Tenant ID. Note the Directory (tenant) ID is their tenant, not ours.
    • Fill in the tenant id, move to the next step, and Azure DevOps generates the Issuer and Subject identifier. Copy both and select Keep as draft. 
    • Send them the Issuer and Subject identifier so they can create the federated credential on their managed identity. When they confirm, they need to send back the following
      • Subscription ID
      • Subscription Name
      • ACR Name
      • ACR Resource Group Name
      • Client ID of their Managed Identity
    • Once the information is received, go back to the draft connection, fill those in, then Finish setup and Verify and save.
One caution: Tenant ID cannot be edited after the connection is created. If it is wrong, you have to delete the connection and create a new one, and therefore a new Subject identifier, so the federated credential has to be recreated as well.

At this point, second service connection should be verified and saved successfully.

Now the moment of truth. We can create a simple pipeline to test the az acr import end-to-end.
trigger: none
pr: none

parameters:
  - name: sourceImageName
    displayName: Source image, repository:tag with no host
    type: string
    default: <IMAGE_NAME>:<IMAGE_TAG>

pool:
  vmImage: ubuntu-latest

variables:
  sourceServiceConnection: <SOURCE_SERVICE_CONNECTION_NAME>
  targetServiceConnection: <EXTERNAL_SERVICE_CONNECTION_NAME>
  sourceAcrLoginServer: <SOURCE_ACR_NAME>.azurecr.io
  targetAcrName: <TARGET_ACR_NAME>
  targetAcrResourceGroup: <TARGET_ACR_RESOURCE_GROUP>

steps:
  - task: AzureCLI@2
    displayName: Get source ACR read token
    inputs:
      azureSubscription: $(sourceServiceConnection)
      scriptType: pscore
      scriptLocation: inlineScript
      inlineScript: |
        az account show `
          --query "{tenant:tenantId, subscription:name, id:id, principal:user.name}" `
          --output table

        $SOURCE_ACCESS_TOKEN = (az account get-access-token `
          --query accessToken `
          --output tsv).Trim()

        Write-Host "##vso[task.setvariable variable=sourceAccessToken;issecret=true]$SOURCE_ACCESS_TOKEN"

  - task: AzureCLI@2
    displayName: Import image into target ACR
    inputs:
      azureSubscription: $(targetServiceConnection)
      scriptType: pscore
      scriptLocation: inlineScript
      inlineScript: |
        az account show `
          --query "{tenant:tenantId, subscription:name, id:id, principal:user.name}" `
          --output table

        az acr import `
          --name "$(targetAcrName)" `
          --resource-group "$(targetAcrResourceGroup)" `
          --source "$(sourceAcrLoginServer)/${{ parameters.sourceImageName }}" `
          --image "${{ parameters.sourceImageName }}" `
          --password "$(sourceAccessToken)" `
          --force

  - task: AzureCLI@2
    displayName: Confirm the tag landed
    inputs:
      azureSubscription: $(targetServiceConnection)
      scriptType: pscore
      scriptLocation: inlineScript
      inlineScript: |
        $IMAGE_NAME = "${{ parameters.sourceImageName }}"

        az acr repository show-tags `
          --name "$(targetAcrName)" `
          --repository ($IMAGE_NAME.Split(":")[0]) `
          --output table
And when you run this, it should do the import.
Pipeline Run
Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, November 14, 2025

Azure DevOps: Azure Functions Core Tools Can't Find .NET 10 Installed by UseDotNet@2 Task on Windows Agents

I was upgrading an Azure Durable Function Application from .NET 9 to .NET 10. Our Azure DevOps pipeline have a job that executes set of integration tests by spinning up the function using Azure Functions Core Tools (func.exe). Since we were using MSSQLLocalDB, the agent is Windows.

After the upgrade, the integration tests was failing to spin up func with a frustrating error.

You must install or update .NET to run this application.
App: D:\a\1\s\tests\...\bin\Debug\net10.0\FunctionApp.dll
Architecture: x64
Framework: 'Microsoft.NETCore.App', version '10.0.0' (x64)
.NET location: C:\Program Files\dotnet\
The following frameworks were found:
  8.0.6 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  8.0.21 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  9.0.6 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  9.0.10 at [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
 
Learn more:
https://aka.ms/dotnet/app-launch-failed
To install missing framework, download:

The pipeline uses the UseDotNet@2 task to install .NET 10.

- task: UseDotNet@2
  displayName: Install .NET 10.0.x
  inputs:
    packageType: 'sdk'
    version: '10.0.x'

The pipeline debug logs showed UseDotNet@2 task was setting DOTNET_ROOT and updating PATH correctly:

##[debug]Absolute path for pathSegments: C:\hostedtoolcache\windows\dotnet\sdk
Successfully installed .NET Core sdk version 10.0.100.
##[debug]Processed: ##vso[task.prependpath]C:\hostedtoolcache\windows/dotnet
##[debug]set DOTNET_ROOT=C:\hostedtoolcache\windows/dotnet
And dotnet --info confirmed .NET 10 was installed.
dotnet --info
However func.exe doesn't seem to recognize it, it kept looking at  C:\Program Files\dotnet. 

When starting the worker process, it ignores:

  • The DOTNET_ROOT environment variable
  • The PATH environment variable

Since .NET 10 isn't yet pre-installed on DevOps agents, Azure Functions can't find it.

After trying different things, the solution came out simple.

When installing .NET 10, override the default installation path which is $(Agent.ToolsDirectory)/dotnet  (C:\hostedtoolcache\windows\dotnet in Windows) to C:\Program Files\dotnet where Azure Functions expects to find it.

- task: UseDotNet@2
  displayName: Install .NET 10.0.x
  inputs:
    packageType: 'sdk'
    version: '10.0.x'
    installationPath: 'C:\Program Files\dotnet'

And that did it. 

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 9, 2025

Azure DevOps: NuGet: Command Not Found with Ubuntu-Latest (24.04)

Our builds started failing today and it's Microsoft Servicing Tuesday even though it's Wednesday over here.

We are using ubuntu-latest in almost all our builds and started seeing the following error:

nuget: command not found

And our build pipelines was using NuGet.

- task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

- script: nuget restore
  displayName: NuGet Restore

And then saw the following warning:

##[warning] See https://aka.ms/azdo-ubuntu-24.04 for changes to the ubuntu-24.04 image. 
Some tools (e.g. Mono, NuGet, Terraform) are not available on the image. 
Therefore some tasks do not run or have reduced functionality.

Have been seen that warning, but didn't put much attention. The link explains the failure.

So it's time to use dotnet restore (too bad we are late to the party)

- task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

- task: DotNetCoreCLI@2
  displayName: .NET Restore
  inputs:
    command: 'custom'
    custom: 'restore'
    projects: '**/*.csproj'

And that's it, we are back to successful builds.

Look out for this one in your builds as well!

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, March 26, 2025

Azure DevOps Classic Release Pipelines: Deploying .NET 9 Isolated Azure Function App on Linux

If you are using Azure DevOps classic releases, you might have noticed, that we still don't have Runtime stack support for DOTNET-ISOLATED|9.0 in Azure Functions Deploy task.
Azure Functions Deploy
So how can we use this to deploy a .NET 9 Isolated Azure Function App on Linux.

It's quite easy, you can just type in DOTNET-ISOLATED|9.0 as the Runtime stack, and upon deployment the correct .NET Version will get set.
Azure Function App on Linux: .NET Version
Hope this helps.

Happy Coding.

Regards,
Jaliya

Monday, February 17, 2025

Azure DevOps Classic Release Pipelines: Read Variables in a Variable Group and Update Azure App Service AppSettings

In this post let's see how to read variables in a Variable Group and deploy them to Azure App Service as app settings from a Classic Azure DevOps Release Pipeline.

In the release pipeline I have Azure CLI task added and the release pipeline is running on Azure Hosted windows-latest agent.

Release Pipeline
In the Azure CLI task, I am doing the following.

Acquire a PAT (Personal Access Token) and set it.

$PAT = "<YOUR_PAT>"
$env:AZURE_DEVOPS_EXT_PAT = $PAT

Now set the default organization and project for az devops command.

az devops configure --defaults `
    organization=https://dev.azure.com/<YOUR_ORGANIZATION>/ `
    project=<YOUR_PROJECT>

Get list of variables in the variable group by Variable Group Id. You can find Variable Group Id in the URL of the Variable Group detail page.

$variablesJson = az pipelines variable-group variable list `
    --group-id <YOUR_VARIABLE_GROUP_ID> `
    --org https://dev.azure.com/<YOUR_ORGANIZATION>/ `
    --project <YOUR_PROJECT>

If we output $variablesJson, it would be something like following.

{
  "SomeOptions__Key1": {
    "isSecret": null,
    "value": "<Value1>"
  },
  "SomeOptions__Key2": {
    "isSecret": null,
    "value": "<Value2>"
  }
}

Convert the $variablesJson to app settings format that Azure App Service expects.

$variablesAppSettings = $variablesJson `
    | ConvertFrom-Json `
    | ForEach-Object { $_.PSObject.Properties } `
    | ForEach-Object ` {
        $key = $_.Name
        $value = $_.Value.value
        @{ 
            name = $key;
            slotSetting = $false;
            value = $value 
        }
}

Save the app settings to a temporary file.

ConvertTo-Json $variablesAppSettings | Out-File "$(System.DefaultWorkingDirectory)\appsettings-updated.json"

appsettings-updated.json would look like below.

[
  {
    "name": "SomeOptions__Key1",
    "value": "<Value1>",
    "slotSetting": false
  },
  {
    "name": "SomeOptions__Key2",
    "value": "<Value2>",
    "slotSetting": false
  }
]

Now finally update the app settings in the web app.

$resourceGroup = "<resourceGroup>"
$webAppName = "<webAppName>"

az webapp config appsettings set `
    --resource-group $resourceGroup `
    --name $webAppName `
    --settings "@$(System.DefaultWorkingDirectory)\appsettings-updated.json"

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, February 5, 2025

Azure DevOps Classic Release Pipelines: Using Task Group Parameters to Control Task Execution

In this post let's see how we can use Parameters in a Task Group to control Task execution in Azure DevOps Classic Release Pipelines.

Let's say we have a Task Group that accepts following parameter.
Task Group Parameter
Now based on the value (true/false) passed in for this parameter, say I want to skip a particular Task. For that, we can use tasks Control Options -> Run this task -> Custom conditions.
Control Options -> Run this task -> Custom conditions
First step is initializing a release level variable with the value of the parameter. Note: I couldn't figure out how to access parameters directly in the condition, hence using a variable. If you find out a way, please do leave a comment. 

We can add in a PoweShell Task and do follows to initialize a variable.
Write-Host "##vso[task.setvariable variable=varIsSkipTask]$(IsSkipTask)"
Set Variable
And now, we can use the variable in custom condition as follows.
and(succeeded(), ne(variables['varIsSkipTask'], 'true'))
Control Options -> Run this task -> Custom conditions: Skip Task
And that's it.

Now when I run a release with IsSkipTask = true ,
IsSkipTask = true
Task is Skipped.
Task is skipped
Else,
Task is not skipped
Task is not getting skipped.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, September 17, 2024

Azure Pipelines: Passing Variables Between Stages

In my last post I wrote about Azure Pipelines: Passing Variables Between Jobs, and in this let's see how we can pass variables between stages in Azure Pipelines.

Let's say we need to pass a variable from StageA to StageB.

trigger:
- main

pool:
  vmImage: ubuntu-latest

stages:
- stage: StageA
  displayName: Stage A
  variables:
    someVariable: ''
  jobs:  
  - job: JobA
    displayName: Job A
    steps:    
    # Create output variable and set value
    - script: |
        echo "##vso[task.setvariable variable=stageAVariable;isOutput=true]someValue"
      name: outputVariable
      displayName: Output Variable

- stage: StageB
  displayName: Stage B
  variables:
    # Map the output variable from Stage A into Stage B
    stageBVariable: $[ stageDependencies.StageA.JobA.outputs['outputVariable.stageAVariable'] ]
  jobs:  
  - job: JobB
    displayName: Job B
    steps:
    # stageBVariable has the value set in JobA
    - script: |
        echo $(stageBVariable)
      name: printUpdatedVariable
      displayName: Print Updated Variable

Note how StageA.JobA.outputVariable is outputting the variable using isOutput=true. And then StageB is getting it mapped via stageDependencies. 

When we have multiple stages in a pipeline, by default, they run sequentially in the order in which they are defined in the YAML file. So in the above use case where we have 2 stages, we don't explicitly need to use dependsOn. 

Now let's add another Step: StageC.

trigger:
- main

pool:
  vmImage: ubuntu-latest

stages:
- stage: StageA
  displayName: Stage A
  variables:
    someVariable: ''
  jobs:  
  - job: JobA
    displayName: Job A
    steps:
    # Create output variable      
    - script: |
        echo "##vso[task.setvariable variable=stageAVariable;isOutput=true]someValue"
      name: outputVariable
      displayName: Output Variable

- stage: StageB
  displayName: Stage B
  variables:
    # Map the output variable from Stage A into Stage B
    stageBVariable: $[ stageDependencies.StageA.JobA.outputs['outputVariable.stageAVariable'] ]
  jobs:  
  - job: JobB
    displayName: Job B
    steps:
    # stageBVariable has the value set in JobA
    - script: |
        echo $(stageBVariable)
      name: printUpdatedVariable
      displayName: Print Updated Variable

- stage: StageC
  displayName: Stage C
  # Need to explictely depend on Stage A
  dependsOn: 
  - StageA
  - StageB
  variables:
    # Map the output variable from Stage A into Stage C
    stageCVariable: $[ stageDependencies.StageA.JobA.outputs['outputVariable.stageAVariable'] ]
  jobs:  
  - job: JobC
    displayName: Job C
    steps:
    # stageCVariable has the value set in JobA
    - script: |
        echo $(stageCVariable)
      name: printUpdatedVariable
      displayName: Print Updated Variable

Here especially for StageC, we need to explicitly depend on StageA. You might skip it thinking they run sequentially meaning StageA -> StageB -> StageC, so technically StageC is depending on StageA.  But unfortunately depending just on the previous stage is not enough. You need to explicitly declare the dependency on the stage from which you are mapping the variable from.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, September 12, 2024

Azure Pipelines: Passing Variables Between Jobs

In this post let's see how we can pass variables between jobs in Azure Pipelines.

Let's say we need to pass a variable from JobA to future jobs.

trigger:
- main/*

pool:
  vmImage: ubuntu-latest

stages:
- stage: StageA
  displayName: Stage A
  jobs:  
  - job: JobA
    displayName: Job A
    steps:
    # Create output variable and set value
    - script: |
        echo "##vso[task.setvariable variable=jobAVariable;isOutput=true]someValue"
      name: outputVariable
      displayName: Output Variable

  - job: JobB
    displayName: Job B
    dependsOn: JobA
    variables:
      # Map the output variable from Job A into Job B
      jobBVariable: $[ dependencies.JobA.outputs['outputVariable.jobAVariable'] ]
    steps:
    # This will print the updated value of the variable
    - script: |
        echo $(jobBVariable)
      name: printUpdatedVariable
      displayName: Print Updated Variable

  - job: JobC
    displayName: Job C
    dependsOn: JobA
    variables:
      # Map the output variable from Job A into Job C
      jobCVariable: $[ dependencies.JobA.outputs['outputVariable.jobAVariable'] ]
    steps:
    # This will print the updated value of the variable
    - script: |
       echo $(jobCVariable)     
      name: printUpdatedVariable
      displayName: Print Updated Variable

Note how StageA.JobA.outputVariable is outputting the variable using isOutput=true. By default jobs run in parallel. So first we need to wait till JobA  completes using dependsOn. Then we can map the variable into any job through dependencies.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Sunday, September 8, 2024

Azure Pipelines: DurableTask.Netherite: Netherite backend requires 64bit, but current process is 32bit.

Recently one of our .NET Isolated Azure Durable Functions got switched to use Netherite as its storage provider and suddenly all the integration tests started to fail in the Azure DevOps CI pipeline. 

The underlying error was:  

DurableTask.Netherite: Netherite backend requires 64bit, but current process is 32bit

In the pipeline, we were using FuncToolsInstaller@0 to install Azure Functions Core Tools.

- task: FuncToolsInstaller@0
  displayName: Install Azure Func Core Tools

Upon investigating, noticed FuncToolsInstaller@0 is using x86 version of Azure Functions Core Tools.

FuncToolsInstaller@0
Checked choco (chocolatey), it was also using x86 as well, fortunately, npm was using x64 by default.
- bash: |
    npm i -g azure-functions-core-tools@4 --unsafe-perm true
  displayName: Install Azure Func Core Tools

And that solved the issue.

Created microsoft/azure-pipelines-tasks enhancement request for FuncToolsInstaller@0.
   [enhancement]: FuncToolsInstaller@0: Use win-x64 for Windows

Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, July 2, 2024

Azure DevOps Pipeline: Build and Deploy Azure Container App

In this post, let's see how we can build and deploy an Azure Container App from an Azure DevOps Pipeline.

Here for deployment, I am using az containerapp update.

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

variables:
  acrServiceConnection: <ACR_SERVICE_CONNECTION>
  acrName: myacr.azurecr.io
  imageRepositoryName: '<ACR_REPOSITORY_NAME>'
  containerAppServiceConnection: '<CONTAINER_APP_SERVICE_CONNECTION>'
  containerAppResourceGroup: '<CONTAINER_APP_RESOURCE_GROUP>'
  containerAppName: '<CONTAINER_APP_NAME>'

name: $(Build.BuildId)

stages:
- stage: Build
  displayName: Build
  jobs:  
  - job: Build
    displayName: Build Docker Image
    steps:
    - task: Docker@2
      displayName: Build and push an image to container registry
      inputs:
        containerRegistry: '$(acrServiceConnection)'
        repository: '$(imageRepositoryName)'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'
        buildContext: './'
        tags: '$(Build.BuildId)'

- stage: Deploy
  displayName: Deploy
  dependsOn:
  - Build
  condition: succeeded('Build')
  jobs:  
  - deployment: Deployment
    displayName: Deploy to Container App
    # Requires an environment named 'Development'
    environment: Development
    strategy:
      runOnce:
        deploy:
          steps:
           - task: AzureCLI@2
             displayName: Update Container App
             inputs:
               azureSubscription: '$(containerAppServiceConnection)'
               scriptType: 'bash'
               scriptLocation: 'inlineScript'
               inlineScript: |
                 az containerapp update \
                 --name $(containerAppName) \
                 --resource-group $(containerAppResourceGroup) \
                 --image '$(acrName)/$(imageRepositoryName):$(Build.BuildId)' \
                 --set-env-vars \
                   'MongoDB__ConnectionString=<VALUE>' \
                   'ServiceBus__ConnectionString=<VALUE>' \
                 --min-replicas 1 \
                 --max-replicas 1

Azure DevOps already has an Azure Container Apps Deployment Task AzureContainerApps@1, which I haven't used, but do check it out.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, February 9, 2024

Azure DevOps Self-hosted Agent: NETSDK1045: The current .NET SDK does not support targeting .NET 8.0

Recently I have faced this issue in one of our Self-hosted agents in Azure DevOps when a pipeline is trying to build a .NET 8.0 application.
C:\vsts-agent\_work\_tool\dotnet\sdk\5.0.405\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(141,5): 

error NETSDK1045: The current .NET SDK does not support targeting .NET 8.0.  
Either target .NET 5.0 or lower, or use a version of the .NET SDK that supports .NET 8.0.  [C:\vsts-agent\_work\67\s\xxxxx.csproj]
The error was happening in a NuGetCommand@2 task while doing a restore. I replaced that with a DotNetCoreCLI@2. Then that step succeeded but eventually failed again in a VSBuild@1 task (that was using vsVersion: '17.0' which is the latest) for the same reason. 

This was strange because the pipeline was specifically requesting for .NET 8.0.
- task: UseDotNet@2
  displayName: Use .NET
  inputs:
    packageType: 'sdk'
    version: '8.0.x'
The pipeline had no reason to use .NET SDK 5.0.405 and had no idea where this specific version was coming from.

Then I started digging, and after scratching my head for a couple of hours, noticed the following in agent worker logs (usually inside C:\vsts-agent\_diag). To my surprise, the pipeline is getting executed with the following.
{
  ...
  "variables": {
    "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR": {
      "value": "C:\\vsts-agent\\_work\\_tool\\dotnet\\sdk\\5.0.405\\Sdks"
    },
    "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER": {
      "value": "5.0.405"
    },
    ...
  }
  ...
}
DOTNET_MSBUILD_SDK_RESOLVER_* are .NET environment variables that are used to force the resolved SDK tasks and targets to come from a given base directory and report a given version to MSBuild.
  • DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR: Overrides the .NET SDK directory.
  • DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER: Overrides the .NET SDK version.
  • DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR: Overrides the dotnet.exe directory path.
And that kind of answered where  .NET SDK 5.0.405 was coming from, but the question remains why. Submitted an Issue #19520: Self hosted agent uses incorrect DOTNET_MSBUILD_SDK_RESOLVER_SDKS_*.

To get past the issue, I had to override these variables. To test the concept, I have overridden these variables by passing .NET 8.0 counterpart values to the pipeline execution.
Passing variables to the pipeline execution
and that finally worked. But we can't be manually overriding these for each run, so I have overridden them in YAML as follows.
variables:
- name: DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR
  value: 'C:\vsts-agent\_work\_tool\dotnet\sdk\8.0.101\Sdks'
- name: DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER
  value: '8.0.101'
...
Now the pipeline builds and publishes .NET 8 apps successfully, but I still have no idea why the older SDK was being forced.

Hopefully, we will find it here soon:

Hope this helps.

Happy Coding.

Regards,
Jaliya