Post

PowerShell Az CLI Helpers

PowerShell Az CLI Helpers

CLI Is Fun For Nerds (And It Is Fast)

System admins, software engineers and other tech professionals will happily bend your ear about the efficiency and hyper-productivity that can be gained from using the command line rather than a visual GUI to do stuff on a computer. What they are less likely to be up front about - but is almost certainly the case every time - is that using the CLI makes us feel good. It’s nostalgic (because using it makes you look like a hacker from an 80s movie) and cool (because using it makes you look like a hacker from an 80s movie). Grimily, it also comes with a bit of “I can do this, you probably can’t”, but a lot of IT tools (and people) are like that so you just have to put up with it. When I put together an effective chain of commands or write a decent wrapper (watch how many times I write that in this article), I feel really useful and like I know what I’m doing, which is a nice feeling to give myself.

Truth is though, fuzzies aside - it is quick, especially for regular or repetitive tasks, and when you combine available CLI tools with a bit of lightweight scripting, you can save yourself a lot of time. By way of an example, here’s an idea for PowerShell Scripts which leverage the Az (Azure) CLI to get things done in the cloud without opening that pesky portal.

There are three main ways to use Azure command line tools: the Cloud Shell (running in the browser), az cli (a standalone command line tool), and Azure PowerShell, a cross-platform powershell module with built-in Azure functionality. Here I use Az CLI (no special reason) - you can definitely achieve this with the powershell module too!

Initial Opportunity

I develop, and then manage, a whole bunch of applications running in Azure, for different clients, across different tenants, in different resources. I have lovely GitHub and Azure DevOps CI/CD pipelines which build either code or container based deployments (via the Azure Container Registry in most cases) automatically.

Mostly, these pipelines automatically deploy to an App Service Slot with a suffix like -release or -staging which I use for user acceptance testing and final checks. Once we’re happy and production-ready, we’ll either a) swap release slots (for code based deployments) or b) update the Container Image in the production slot (for container based deployments).

This is very easily done through the Azure Portal. You can swap slots by navigating to the Web App resource and hitting the “Swap Button”, and you can deploy a new container image from the same resource’s Deployment Center.

But when you have to do it across multiple web apps per deployment, it’s a little boring and slow. I don’t want to waste time jumping around between tabs, re-authenticating in the portal and all that. So let’s speed that up.

The Az CLI Commands

Of course, the AZ CLI is sitting there demurely, watching me flailing around clumsily in a browser, just waiting to be asked to help. Begging, almost, with the simplicity of the tools it has to offer.

You have to be authenticated in the tenant and authorized with RBAC to take the actions below. az login gets you there without any fuss, generally.

Swap Slots

Want to swap slots? Great. All you need is the resource group, app name, and the names of both slots:

1
az webapp deployment slot swap --resource-group rg-name --name app-name --slot source-slot --target-slot target-slot

That’s really all there is to it. If you’ve got the right details and permissions, it’ll happen.

Deploy Container Image

This is also simple, but depending on how you name your images, getting the right image name can add a fetch step.

To deploy a container image, all you need is:

1
az webapp config container set --resource-group grou-name --name app-name --container-image-name image-name

But perhaps you don’t have the name to hand - and note that the container-image-name needs to be fully qualified for this to work. I often use a pattern where my images are something like app-name:x.x.x-y where x.x.x is the version and y is the build number generated by the pipeline. I’m not that smart so I can’t mentally keep track of the build numbers as they’re pushed up to the Azure Container Repository. Fortunately, the acquiescent AZ CLI can help me with this too:

1
az acr repository show-tags --name registry-name --repository repo-name --orderby time_desc --top 2 

What’s happening here? I’m directly contacting the ACR Repo where I push my images and fetching the latest tags. time_desc naturally orders the images in reverse chronological order, and --top 2 limits the results to the first two. Why’s that? Because when my pipeline builds images, it generates two tags: the stable version tag (mentioned above) and the ubiquitous latest tag. My staging/release environments are usually configured to build off the latest tag so they’re always up to date, but I want to keep production slots pinned to a stable version. Thus when I pull my image list, I want to grab the top two to make sure both the latest and the stable tag are included.

Wrap Them In PowerShell Scripts

That’s great but it’s lots of typing! Typing is faster than using the GUI, but not-typing is faster still, so I wrote a couple of scripts to do this for me.

Container Images 1: Get the Latest Tag

Let’s define the function so we can make it flexible by passing in reg and repo names as arguments:

1
2
3
4
5
6
7
8
9
10
function Get-StableImageTag {
    param(
        [Parameter(Mandatory)]
        [string] $RegistryName,

        [Parameter(Mandatory)]
        [string] $Repository
    )
    ...
    }

(Sometimes I’ll write wrapper scripts like this and then write another wrapper which passes in my client specific details at a keystroke. I’m wrap, wrap, wrapping.)

Next let’s run the az cli command but gather the results into a variable:

1
2
3
4
5
6
 $tags = az acr repository show-tags `
        --name $RegistryName `
        --repository $Repository `
        --orderby time_desc `
        --output json `
        --top 2 | ConvertFrom-Json

Note the pipe to ConvertFrom-Json at the end. az cli returns JSON by default, which PowerShell is able to convert into native PS objects thanks to this handy helper. This means the $tags variable is populated with a collection of (two) powershell objects, which I can filter using the Where-Object function:

1
$latestTag = $tags | Where-Object { $_ -NotMatch "latest" } | Select-Object -First 1

We start with two objects, get a list which doesn’t include latest using -NotMatch , and select the first object (of one objects).

That means the output (assuming we find any tags) is my stable version tag: 1.1.1-2020, or whatever it might be:

1
2
3
4
5
6
if ($latestTag) {
        Write-Host "Latest tag: $latestTag"
        return $latestTag
    } else {
        Write-Host "No tags found in repository."
    }

And the full version of the script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Get-StableImageTag {
    param(
        [Parameter(Mandatory)]
        [string] $RegistryName,

        [Parameter(Mandatory)]
        [string] $Repository
    )

    Write-Host "Fetching latest tag from $Repository in $RegistryName..."

    $tags = az acr repository show-tags `
        --name $RegistryName `
        --repository $Repository `
        --orderby time_desc `
        --output json `
        --top 2 | ConvertFrom-Json

    $latestTag = $tags | Where-Object { $_ -NotMatch "latest" } | Select-Object -First 1

    if ($latestTag) {
        Write-Host "Latest tag: $latestTag"
        return $latestTag
    } else {
        Write-Host "No tags found in repository."
    }
}

Container Images 2: Deploy the Tag

Now we have the tag, it’s easy. Again, let’s define the function with some arguments so it’s flexible:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Update-ContainerImage {
    param(
        [Parameter(Mandatory)]
        [string] $ResourceGroup,

        [Parameter(Mandatory)]
        [string] $AppName,

        [Parameter(Mandatory)]
        [string] $RegistryName,

        [Parameter(Mandatory)]
        [string] $Repository
    )

Then the meat of it is simple thanks to the helper we just made:

1
2
3
4
5
6
7
8
$latestTag = Get-LatestVersionImageTag -RegistryName $RegistryName -Repository $Repository

$image = "${RegistryName}.azurecr.io/${Repository}:${latestTag}"

    az webapp config container set `
        --resource-group $ResourceGroup `
        --name $AppName `
        --container-image-name $image

We use the tag label, registry name and repository to build a fully qualified url for the image, then give it over to az cli.

For the full version, I just add some basic error handling, and we’re done:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Update-ContainerImage {
    param(
        [Parameter(Mandatory)]
        [string] $ResourceGroup,

        [Parameter(Mandatory)]
        [string] $AppName,

        [Parameter(Mandatory)]
        [string] $RegistryName,

        [Parameter(Mandatory)]
        [string] $Repository
    )

    try {
        $latestTag = Get-LatestVersionImageTag -RegistryName $RegistryName -Repository $Repository
        if ($latestTag) {
            $image = "${RegistryName}.azurecr.io/${Repository}:${latestTag}"
            Write-Host "Deploying $image to $AppName in resource group $ResourceGroup..."
            az webapp config container set `
                --resource-group $ResourceGroup `
                --name $AppName `
                --container-image-name $image
            Write-Host "Deployment initiated."
        }
    }
    catch {
        Write-Error "Failed to deploy image: $_"
    }
}

Swap Slots

Easy, right? Swapping slots is even easier - arguably not even worth it when the az cli command is so simple - but nonetheless here’s the PS wrapper I use for that, which does a bit of try/catch and gives me some output to follow along with.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Swap-WebAppSlots {
    param (
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroup,

        [Parameter(Mandatory=$true)]
        [string]$AppName,

        [Parameter(Mandatory=$true)]
        [string]$SourceSlot,

        [Parameter(Mandatory=$true)]
        [string]$TargetSlot
    )

    try {
        Write-Host "Swapping slots: $SourceSlot -> $TargetSlot for $AppName..."
        az webapp deployment slot swap `
            --resource-group $ResourceGroup `
            --name $AppName `
            --slot $SourceSlot `
            --target-slot $TargetSlot
        Write-Host "Swap completed successfully."
    }
    catch {
        Write-Error "Failed to swap slots: $_"
    }
}

Wrapping Up: The Payoff

Great, so I spent some time writing scripts which probably took at least as long as doing this via the portal a few times. What’s the point (aside from what I mentioned up top, which is it makes me feel cool and useful)? The point is for my three-apps-together service (an API, an internal dashboard and a public website), I can update them all simultaneously with this:

1
2
3
4
 $apps = @("client-dashboard","client-website","client-api")
  foreach ($app in $apps) {
      Update-ContainerImage -ResourceGroup "client-rg" -AppName $app -RegistryName "client-acr" -Repository $app
  }

If you’ve waded this far in, it won’t surprise you to learn that I have put the above code into… another wrapper. Thanks for reading!

This post is licensed under CC BY 4.0 by the author.