DEV Community

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

Posted on • Edited on

1

Running and Conserving Azure PowerShell in Azure Container Instances

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!

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs