The Ops Community ⚙️

Cover image for Running and Conserving Azure PowerShell in Azure Container Instances
Kai Walter
Kai Walter

Posted on

Running and Conserving Azure PowerShell in Azure Container Instances

TL;DR

In this post I show how to spin up an Azure Container Instance (ACI) with the standard Azure PowerShell Docker image while maintaining state / persisting the context to an Azure Storage Account (File Share).

Motivation

I have to switch between Azure environments (e.g. Global vs. China) and accounts (corporate, private trial) quite often. When switching I have to set the desired subscription again. Already using ACI for other temporary scenarios e.g. Handling an ACME challenge response with a temporary Azure Container Instance or Creating a Certificate Authority for testing with Azure Container Instances I thought about trying to get Azure PowerShell running while conserving the context (logged on session) to an Azure Storage.

The Script

To achieve the above I created this script which creates and populates an Azure Storage File Share, starts, initializes, runs and deletes the container instance:

param (
    [Switch]
    $SkipStartup
)

$aciName = "{my-desired-aci-name}"
$storageName = $aciName.Replace("-", "")
$resourceGroupName = "{my-desired-existing-resource-group-name}"

$storageContainerName = "state"
$storageShareName = $storageContainerName
$containerStateFolder = "/tmp/state"
$imageName = "mcr.microsoft.com/azure-powershell:latest"
$startupScriptName = "startup.ps1"

# Part 1 - Azure File Share creation

if (!$(az storage account list -g $resourceGroupName --query "[?name == '$storageName']" -o tsv)) {
    az storage account create -n $storageName -g $resourceGroupName
}

$storageConnectionString = $(az storage account show-connection-string -n $storageName -o tsv) 
$storageKey = $(az storage account keys list -n $storageName --query [0].value -o tsv)

if (!$(az storage share list --account-name $storageName --connection-string $storageConnectionString --query "[?name == '$storageShareName']"-o tsv)) {
    az storage share create --account-name $storageName --connection-string $storageConnectionString -n $storageShareName
}

# Part 2 - Startup script

$script = Join-Path $env:TEMP "azContainerStartup.ps1"
@'
foreach($linkName in ".Azure",".IdentityService") {
    if($linkName -eq ".Azure") {
        $linkPath = "$HOME/$linkName"
    } else {
        $linkPath = "$HOME/.local/share/$linkName"
    }
    $linkTargetPath = "{containerStateFolder}/$linkName"

    if(!(Test-Path $linkTargetPath -PathType Container)) {
        Write-Host "create" $linkTargetPath
        New-Item -Path $linkTargetPath -ItemType Directory
    }
    if((Get-ChildItem -Path $linkTargetPath -Force).Count -eq 0 -and (Test-Path -Path $linkPath -PathType Container)) {
        Write-Host "copy from" $linkPath "to" $linkTargetPath
        Copy-Item -Path "$linkPath/*" -Destination $linkTargetPath -Recurse -Force
    }

    $linkItem = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
    if($linkItem) {
        if(!$($linkItem.LinkType)) {
            Write-Host "remove existing folder to create link" $linkPath
            Remove-Item $linkPath -Recurse -Force
        }
    }
    New-Item -ItemType SymbolicLink -Path $linkPath -Target $linkTargetPath
}
'@.Replace("{containerStateFolder}", $containerStateFolder) | Set-Content $script -Force

az storage file upload -s $storageShareName --source $script -p $startupScriptName `
    --account-name $storageName --connection-string $storageConnectionString

# Part 3 - Container startup

az container create --name $aciName `
    --resource-group $resourceGroupName `
    --image $imageName `
    --azure-file-volume-account-name $storageName `
    --azure-file-volume-account-key $storageKey `
    --azure-file-volume-share-name $storageShareName `
    --azure-file-volume-mount-path $containerStateFolder `
    --command-line "tail -f /dev/null"

if (!$SkipStartup) {
    az container exec  --name $aciName `
        --resource-group $resourceGroupName `
        --exec-command "pwsh $containerStateFolder/$startupScriptName"
}

# Part 4 - Container operation and destruction

az container exec  --name $aciName `
    --resource-group $resourceGroupName `
    --exec-command "pwsh"

az container delete --name $aciName `
    --resource-group $resourceGroupName 
Enter fullscreen mode Exit fullscreen mode

I started with this PowerShell/Azure CLI combination to transfer from existing scripts more easily. Later I converted to a pure PowerShell/Azure PowerShell version but ran into problems during pwsh shell execution : terminal control sequences were not rendered correctly and did not allow viable operation of the container.

Part 1 - Azure File Share creation

Creates the storage account and/or file share, in case those are not yet present.

Part 2 - Startup script

Generates and uploads a startup script startup.ps1 to the file share, that:

  • for the 2 folders representing Azure PowerShell context ~/.Azure and ~/.local/share/.IdentityService
    • creates the folder on the file share (mounted volume) to persist state
    • copies content from initial container to mounted volume in case it is not yet populated
    • removes folder on container
    • creates symbolic link from ~ to mounted volume

Hence with a fresh file share this will copy initial contents from container and then link the folders. With an already populated file share it will just link the folders.

Part 3 - Container startup

ACI (container group) is started, volume is mounted and container is sent into an endless loop with tail -f /dev/null to stay up and wait until it is contacted again to execute actual commands.

Once started the startup script startup.ps1 is execute (see above).

Part 4 - Container operation and destruction

Now PowerShell is executed and will wait for terminal input.

Connect-AzAccount -UseDeviceAuthentication (or as in my case Connect-AzAccount -Environment AzureChinaCloud -UseDeviceAuthentication) can now be used to initialize and login to Azure.

With exit the Azure PowerShell session is stopped and the container is deleted.

When re-executing this script it should be possible (given the access token has not expired) to continue working in the same environment and subscription - at least it worked during my testing.

very nice feature : auto completion is working within the container

Potential extensions

For real operations this approach is still lacking that a repository of scripts is not available in the container session. This can be achieved by either copying required scripts to the file share and link it to a proper place or to use az container create parameters --gitrepo-url ... --gitrepo-dir ... --gitrepo-mount-path ....

Azure PowerShell version

Maybe you get more lucky in your region and it works. I had problems in the past with various regions operating ACI on different backplanes (one of them "Atlas" and the other I don't remember).

For interested parties - this is what it looks like:

Screen with erratic control characters

param (
    [Switch]
    $SkipStartup
)

$aciName = "{my-desired-aci-name}"
$storageName = $aciName.Replace("-", "")
$resourceGroupName = "{my-desired-existing-resource-group-name}"

$storageContainerName = "state"
$storageShareName = $storageContainerName
$containerStateFolder = "/tmp/state"
$imageName = "mcr.microsoft.com/azure-powershell:latest"
$startupScriptName = "startup.ps1"

# Part 1 - Azure File Share creation

$resourceGroup = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction Stop

if (!(Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageName -ErrorAction SilentlyContinue)) {
    New-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageName `
        -Location $resourceGroup.Location `
        -SkuName Standard_RAGRS -Kind StorageV2
}

$storageKey = (Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -Name $storageName)[0].Value
$storageContext = New-AzStorageContext -StorageAccountName $storageName -StorageAccountKey $storageKey

if (!(Get-AzStorageShare -Context $storageContext -Name $storageShareName -ErrorAction SilentlyContinue)) {
    New-AzStorageShare -Context $storageContext -Name $storageShareName
}

# Part 2 - Startup script

$script = Join-Path $env:TEMP "azContainerStartup.ps1"
@'
foreach($linkName in ".Azure",".IdentityService") {
    if($linkName -eq ".Azure") {
        $linkPath = "$HOME/$linkName"
    } else {
        $linkPath = "$HOME/.local/share/$linkName"
    }
    $linkTargetPath = "{containerStateFolder}/$linkName"

    if(!(Test-Path $linkTargetPath -PathType Container)) {
        Write-Host "create" $linkTargetPath
        New-Item -Path $linkTargetPath -ItemType Directory
    }
    if((Get-ChildItem -Path $linkTargetPath -Force).Count -eq 0 -and (Test-Path -Path $linkPath -PathType Container)) {
        Write-Host "copy from" $linkPath "to" $linkTargetPath
        Copy-Item -Path "$linkPath/*" -Destination $linkTargetPath -Recurse -Force
    }

    $linkItem = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
    if($linkItem) {
        if(!$($linkItem.LinkType)) {
            Write-Host "remove existing folder to create link" $linkPath
            Remove-Item $linkPath -Recurse -Force
        }
    }
    New-Item -ItemType SymbolicLink -Path $linkPath -Target $linkTargetPath
}
'@.Replace("{containerStateFolder}", $containerStateFolder) | Set-Content $script -Force

Set-AzStorageFileContent -Context $storageContext -ShareName $storageShareName `
    -Source $script -Path $startupScriptName `
    -Force

# Part 3 - Container startup

$volume = New-AzContainerGroupVolumeObject -Name "state" -AzureFileShareName $storageShareName `
    -AzureFileStorageAccountName $storageName `
    -AzureFileStorageAccountKey $(ConvertTo-SecureString $storageKey -AsPlainText -Force)
$mount = New-AzContainerInstanceVolumeMountObject -MountPath $containerStateFolder -Name "state"

$container = New-AzContainerInstanceObject -Name "pwsh" `
    -Image $imageName `
    -VolumeMount $mount `
    -Port @() `
    -Command "tail","-f","/dev/null"

$containerGroup = New-AzContainerGroup -Name $aciName `
    -ResourceGroupName $resourceGroupName `
    -Location $resourceGroup.Location `
    -Container $container `
    -Volume $volume

if (!$SkipStartup) {
    Invoke-AzContainerInstanceCommand -ContainerGroupName $aciName `
        -ResourceGroupName $resourceGroupName `
        -ContainerName "pwsh" `
        -Command "pwsh $containerStateFolder/$startupScriptName"
}

# Part 4 - Container operation and destruction

Invoke-AzContainerInstanceCommand -ContainerGroupName $aciName `
    -ResourceGroupName $resourceGroupName `
    -ContainerName "pwsh" `
    -Command "pwsh"

Remove-AzContainerGroup -ContainerGroupName $aciName `
    -ResourceGroupName $resourceGroupName
Enter fullscreen mode Exit fullscreen mode

It took me a while to figure out that New-AzContainerInstanceObject needed the command as an array and would not operate with the whole command in a string!

Top comments (0)