Running and Conserving Azure PowerShell in Azure Container Instances
Tags: azure
Published: 2022-10-03 17:55:57

---
title: Running and Conserving Azure PowerShell in Azure Container Instances
published: true
description: In this post I show how to spin up an Azure Container Instance (**ACI**) with the [standard Azure PowerShell Docker image](https://hub.docker.com/_/microsoft-azure-powershell) while maintaining state / persisting the context to an Azure Storage Account (File Share).
tags: #azure
cover_image: https://community.ops.io/images/bfMSR1eq_CIrStrGKY0PewaIqinYG8w8gOgPO-RIdBY/s:1000:420/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2V1N2Qw/YnZoNWF2Z2VyMHls/ZTJxLmpwZw
---

## 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](https://community.ops.io/kaiwalter/handling-an-acme-challenge-response-with-a-temporary-azure-container-instance-2f2d) or [Creating a Certificate Authority for testing with Azure Container Instances](https://community.ops.io/kaiwalter/creating-a-certificate-authority-for-testing-with-azure-container-instances-27ch) 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:

```PowerShell
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 
```

> 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](https://community.ops.io/remoteimages/uploads/articles/mf9heettjooexkexdcna.png)

```PowerShell
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
```

> 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!