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
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:
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!
Top comments (0)