0 Comments Posted in:

Recently I blogged about how you can automate the creation of a Minecraft server on Azure Container Instances. I showed how we could use PowerShell to start and stop the container instance, allowing us to keep costs to a minimum.

What I want to do next is provide my children with an easy way to start and stop the Minecraft server, just by clicking a secret link, rather than me needing to do it for them with Azure PowerShell.

I also would like to automate shutdown of the server, so that if they forget to switch it off, it will switch off automatically a few hours after being started.

There are plenty of ways we could go about automating this. I recently blogged about automating Azure Container Instances with C# Azure Functions, and Logic Apps would also be a perfectly good solution.

But I decided that this would be the ideal opportunity for me to try out the new PowerShell support for Azure Functions v2. This comes with the PowerShell Az module ready installed, and automatically authenticates with Azure using a managed identity, so it makes it trivially easy to automate Azure resource management tasks.

In fact, Anthony Chu has already provided a great sample of using PowerShell Azure Functions to create Azure Container Instances, as well as showcasing how to integrate with queues. My needs were slightly different: I want to schedule container shutdown, and I'm starting existing containers, not creating them from scratch.

Step 1 - Creating an Azure Functions PowerShell App

The first thing we need to do is create an Azure Functions App to host our PowerShell. Here's a script showing how to use the Azure CLI to create a Function App with powershell set as the worker type. It also creates a Storage Account and App Insights instance to use.

Finally, I add the resource group name and container name to the Function App as app settings. These can be accessed as environment variables in the Functions PowerShell script later.

$resourceGroup = "MinecraftTest"
$location = "westeurope"
az group create -n $resourceGroup" `
    -l $location

$functionAppName = "MinecraftFuncsTest"
$funcStorageAccountName = "minecraftfuncstest"
az storage account create `
  -n $funcStorageAccountName `
  -l $location `
  -g $resourceGroup `
  --sku Standard_LRS

$appInsightsName = "MinecraftInsights"
az resource create `
  -g $resourceGroup -n $appInsightsName `
  --resource-type "Microsoft.Insights/components" `
  --properties '{\"Application_Type\":\"web\"}'

az functionapp create `
  -n $functionAppName `
  --storage-account $funcStorageAccountName `
  --consumption-plan-location $location `
  --app-insights $appInsightsName `
  --runtime powershell `
  -g $resourceGroup

az functionapp config appsettings set -n $functionAppName -g $resourceGroup `
  --settings "ContainerName=$containerName" "ResourceGroup=$resourceGroup"

Step 2 - Configure a Managed Identity

The Function App needs permission to start and stop container groups and for that we'll need to create a managed identity. I've written before about how to set this up, so I'll just show the code here. I'm granting the managed identity Contributor access for the specific ACI "container group" I want to start and stop, whose name is already stored in $containerGroup. But I've also shown how to define a broader resource group scope in case you wanted your Function App to be able to create additional container groups in that resource group.

az functionapp identity assign `
    -n $functionAppName -g $resourceGroup

$principalId = az functionapp identity show -n $functionAppName `
  -g $resourceGroup --query principalId -o tsv

$subscriptionId = az account show --query "id" -o tsv
$resourceGroupScope = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup"
$containerScope = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.ContainerInstance/containerGroups/$containerName"

az role assignment create --role "Contributor" `
    --assignee-object-id $principalId `
    --scope $containerScope

Step 3 - Create a PowerShell Function App project

There are a few ways to create a new PowerShell project, but probably the easiest is to use the Azure Functions extension in Visual Studio Code. There are good instructions here on the official Microsoft docs, so I won't go into any more detail other than saying its just a matter of clicking "Create New Project" and selecting the PowerShell runtime.

Options for scheduling shutdown

There are a few different ways we could tackle the challenge of shutting down the container group. My first idea was to send a future scheduled message on an Azure Storage Queue whenever we start a container. Sadly, the Azure Functions bindings don't offer a way to send scheduled messages to Storage Queues, so it turns out to be a bit complex to implement.

Another obvious option would be to use Azure Durable Functions workflows. Every time we start the Minecraft server, it could start a workflow, and then sleep for a few hours before shutting it down. It could also easily allow an external event to shut it down early on demand. I'd certainly use this approach if I was using C#, but it's not possible with PowerShell at the moment.

The final and simplest option is to just have a timer, and when it fires it decides if the container should be shut down. I settled on an approach where I put a text file in blob storage with the scheduled shutdown time, and if the timer fires after that time, it stops the container. Once the container has been stopped, we clear out the shutdown time in blob storage to save us from doing unnecessary work trying to talk to an already stopped container group.

Step 4 - Common functions

I wanted to put my reusable PowerShell utility functions into a shared script, and after a couple of failed attempts, I settled on putting them in profile.ps1 which runs once whenever a new server starts hosting our Function App.

This is also where code for the automatic connection to Azure lives (assuming you've given your Function App a Managed Identity). Here are my Get-AccessToken and Send-ContainerGroupCommand functions I discussed in my previous post.

if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) {
    Connect-AzAccount -Identity
}

function Get-AccessToken($tenantId) {
    $azureRmProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile;
    $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azureRmProfile);
    $profileClient.AcquireAccessToken($tenantId).AccessToken;
}

function Send-ContainerGroupCommand($resourceGroupName, $containerGroupName, $command) {
    $azContext = Get-AzContext
    $subscriptionId = $azContext.Subscription.Id
    $commandUri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/$containerGroupName/$command" + "?api-version=2018-10-01"
    $accessToken = Get-AccessToken $azContext.Tenant.TenantId
    $response = Invoke-RestMethod -Method Post -Uri $commandUri -Headers @{ Authorization="Bearer $accessToken" }
    $response
}

Step 4 - The starter function

The container starter function is simply a HTTP triggered function with "function" level security - so you can only call it if you have the secret code.

The function simply calls our Send-ContainerGroupCommand method, calculates the time to schedule an automatic shutdown (hard-coded to four hours in the future) and then returns an OK response.

I added an additional blob output binding to the function which is used to write that scheduled shutdown time into a text file in blob storage.

param($Request, $TriggerMetadata)

$containerName = $env:ContainerName
$resourceGroup = $env:ResourceGroup
Send-ContainerGroupCommand -resourceGroupName $resourceGroup -containerGroupName $containerName -command "start"

$schedule = [System.DateTimeOffset]::Now.AddHours(4).ToString("o")

$status = [HttpStatusCode]::OK
$body = "Started $containerName will shut-down after $schedule"

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

Push-OutputBinding -Name ScheduleBlob -Value $schedule

To enable the blob output binding I had to add the following JSON to the function.json

{
    "name": "ScheduleBlob",
    "type": "blob",
    "direction": "out",
    "path": "scheduler/shutdown.txt",
    "connection": "AzureWebJobsStorage"
}

Step 5 - The stop function

I also created a function to stop the container on demand. This does almost the same thing, and overwrites the scheduled shutdown file contents in blob storage to save our scheduled function from attempting to stop it again.

param($Request, $TriggerMetadata)

$containerName = $env:ContainerName
$resourceGroup = $env:ResourceGroup
Send-ContainerGroupCommand -resourceGroupName $resourceGroup -containerGroupName $containerName -command "stop"

$status = [HttpStatusCode]::OK
$body = "Stopped $containerName"

Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

Push-OutputBinding -Name ScheduleBlob -Value "STOPPED"

Step 6 - the scheduled function

Using the Azure Functions extension for Visual Studio Code, it's easy to add a timer triggered function. I added a blob input and output binding, so each time the timer fired, I could read the scheduled shutdown time, and update it if necessary. I set my timer trigger to run every 4 hours.

Here's the function.json file:

{
  "bindings": [
    {
      "name": "Timer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 */4 * * *"
    },
    {
      "name": "InBlob",
      "type": "blob",
      "direction": "in",
      "path": "scheduler/shutdown.txt",
      "connection": "AzureWebJobsStorage",
      "dataType": "string"
    },
    {
      "name": "OutBlob",
      "type": "blob",
      "direction": "out",
      "path": "scheduler/shutdown.txt",
      "connection": "AzureWebJobsStorage",
      "dataType": "string"
    }
  ]
}

And here's the PowerShell, that parses the scheduled shutdown date, requests the container stops if its past its due date, and updates the blob contents to DONE if we've shut down the container group.

param($Timer, $InBlob)

[System.DateTimeOffset]$scheduleDate = New-Object System.DateTimeOffset
$OutValue = $InBlob

# Check that directory name could be parsed to DateTime
if ([System.DateTimeOffset]::TryParse($InBlob, [ref]$scheduleDate)) {
    if ([System.DateTimeOffset]::Now -gt $scheduleDate) {
        $containerName = $env:ContainerName
        $resourceGroup = $env:ResourceGroup
        Write-Host "Requesting shutdown of $containerName in resource group $resourceGroup."
        Send-ContainerGroupCommand -resourceGroupName $resourceGroup -containerGroupName $containerName -command "stop"
        $OutValue = "DONE"
    }
    else {
        Write-Host "Shutdown due in future [$InBlob]"
    }
}
else {
    Write-Host "No shutdown scheduled date [$InBlob]"
}

if ($OutValue) {
    Push-OutputBinding -Name OutBlob -Value $OutValue
}

Taking it further

Overall, working with PowerShell in Azure Functions is relatively straightforward even for someone like me who's not a PowerShell expert. I think that the addition of PowerShell support to Azure Functions is a great step forward and will open the door to lots of interesting automation scenarios that are fiddly to implement in other languages.

This solution should work just fine as it is, but if I were to take it further, it should probably have better error handling, and it would be nice to create a dashboard web page, where you could see the current state of the container group, view its logs, and request a start or stop.

Finally, the use of a timer trigger for shutdown was a bit of a compromise. Durable Functions would be the right way to go on this and would be a good improvement when PowerShell support becomes available.

Want to learn more about how easy it is to get up and running with Azure Container Instances? Be sure to check out my Pluralsight course Azure Container Instances: Getting Started.

0 Comments Posted in:

All of my children are big fans of Minecraft, and recently my son asked me to help him set up a Minecraft server so he could play online with his friends. Obviously, one option would have been to create a Virtual Machine and install the Minecraft server on that. However, that's quite an expensive option - at about £30 a month for a virtual machine that would sit idle for most of the time.

Now of course in Azure a VM can be put into a "stopped deallocated" state, during which you are not charged for compute. But when you restart the VM, you have a bit of a wait for boot-up, and it will have a different IP address, which my son would then need to communicate to all his friends.

And this is actually a great use case for Azure Container Instances. We can simply ask Azure to spin up a container running the Minecraft server (using a Minecraft server image from Docker Hub), use it, and stop it when we've finished. ACI containers start and stop quickly, and we can assign them a friendly domain name that will remain the same when we start up again. And so long as we map the data folder to an Azure Storage File Share, all the state stored on the server will be persisted while our container is not running. This keeps our costs to a minimum.

In this post, I'll show you how we can automate the creation of all the infrastructure we need with the Azure Az PowerShell module. Normally my preference would be to do this with the Azure CLI, but I'm opting to do this all with the PowerShell module, for reasons I'll explain in a bit.

Get started with Az PowerShell

First, if like me you're fairly new to the Az PowerShell module, here's the basic commands you'll need to get logged in, select your subscription, and explore your resource groups:

# login to Azure PowerShell
Connect-AzAccount

# see what subscriptions are available
Get-AzContext -ListAvailable

# to see the current subscription name
$azContext = Get-AzContext
$azContext.Subscription.Name

# select the subscription we want to use
Set-AzContext -SubscriptionName "My Azure Subscription"

# see what resource groups we have
Get-AzResourceGroup | select ResourceGroupName

Now, let's create a Resource Group to hold the resources we'll create in this demo with New-AzResourceGroup:

# create a resource group
$resourceGroupName = "MinecraftTest"
$location = "westeurope"
New-AzResourceGroup -Name $resourceGroupName -Location $location

Creating a Storage Account and File Share

To ensure that we can persist the state of the server, we need to create Storage Account and a file share within that Storage Account. I've created a PowerShell function called SetupStorage which uses New-AzStorageAccount to create a Storage Account and New-AzStorageShare to create a file share. I wanted this function to be idempotent and not fail if the Storage Account and share already existed, and the way I did that was to use the Get- methods first, with the -ErrorAction SilentlyContinue flag set.

Once we've ensured that the Storage Account and the file share are present, we need to get hold of the Storage Account key with Get-AzStorageAccountKey and turn that into a PSCredential object which we'll need to mount the share as a volume on our container.

function SetupStorage {
    param( [string]$StorageResourceGroupName, 
           [string]$StorageAccountName, 
           [string]$ShareName,
           [string]$Location)

    # check if storage account exists
    $storageAccount = Get-AzStorageAccount `
        -ResourceGroupName $StorageResourceGroupName `
        -Name $StorageAccountName `
        -ErrorAction SilentlyContinue

    if ($storageAccount -eq $null) {
        # create the storage account
        $storageAccount = New-AzStorageAccount `
            -ResourceGroupName $StorageResourceGroupName `
            -Name $StorageAccountName `
            -SkuName Standard_LRS `
            -Location $Location
    }

    # check if the file share already exists
    $share = Get-AzStorageShare `
        -Name $ShareName -Context $storageAccount.Context `
        -ErrorAction SilentlyContinue

    if ($share -eq $null) {
        # create the share
        $share = New-AzStorageShare `
            -Name $ShareName `
            -Context $storageAccount.Context
    }

    # get the credentials
    $storageAccountKeys = Get-AzStorageAccountKey `
        -ResourceGroupName $StorageResourceGroupName `
        -Name $StorageAccountName

    $storageAccountKey = $storageAccountKeys[0].Value
    $storageAccountKeySecureString = ConvertTo-SecureString $storageAccountKey -AsPlainText -Force
    $storageAccountCredentials = New-Object System.Management.Automation.PSCredential ($storageAccountName, $storageAccountKeySecureString)
    
    return $storageAccountCredentials
}

Now we have the SetupStorage function, let's pick a name for the Storage Account and file share and get hold of the credentials.

$storageAccountName = "minecraft20190514" # must be unique across azure
$shareName = "minecraft"
$storageAccountCredentials = SetupStorage `
    -StorageResourceGroupName $resourceGroupName `
    -StorageAccountName $storageAccountName `
    -ShareName $shareName `
    -Location $location

Creating an ACI Container Group

Now we're ready to create the container group that will run the Minecraft server. First we need to pick a name for the container group and a unique DNS name prefix to give our Minecraft server a friendly DNS name. We also need to set up some environment variables - one to accept the EULA, and one to set a particular Minecraft user as the admin for this server (this will get written into the ops.json file in the file share the first time this container starts up).

We can create the container group with New-AzContainerGroup and the DOcker image we're using is itzg/minecraft-server from Docker Hub which is a well maintained Minecraft server image with options to configure Bukkit and Spigot (not claiming to know what they are, but my kids tell me they're good!). I need to ensure the container group has a public IP address, and mounts the file share to the /data path. We also need to open the default Minecraft server port of 25565.

$containerGroupName = "minecraft20190514"
$dnsNameLabel = "minecrafttest"
$environmentVariables = @{ EULA = "TRUE"; OPS = "YourMinecraftUserName";
}

New-AzContainerGroup -ResourceGroupName $resourceGroupName `
    -Name $containerGroupName `
    -Image "itzg/minecraft-server" `
    -AzureFileVolumeAccountCredential $storageAccountCredentials `
    -AzureFileVolumeShareName $shareName `
    -AzureFileVolumeMountPath "/data" `
    -IpAddressType Public `
    -OsType Linux `
    -DnsNameLabel $dnsNameLabel `
    -Port 25565 `
    -EnvironmentVariable $environmentVariables

And with that one command, our Minecraft server will be up and running. It will start up very quickly, and we can check up on its status with Get-AzContainerGroup which will tell us the fully qualified domain name of the server.

# check up on the status
$containerGroup = Get-AzContainerGroup -ResourceGroupName $resourceGroupName -Name $containerGroupName

# view the domain name (e.g. minecrafttest.westeurope.azurecontainer.io)
$containerGroup.Fqdn

Starting and stopping the container group

I was hoping that the PowerShell Az module would have a nice and simple command to start and stop container groups like you can with the Azure CLI's az container stop and az container start commands. Unfortunately equivalent commands haven't been created for the Azure Az PowerShell module yet (please vote for my feature request here).

How can we start and stop the container without built-in commands? Well, we can call the Azure REST API's directly, such as this API to stop a container group. However, we need to provide a bearer token, and this turned out to be a challenge to get hold of. After a bit of experimenting, I found that the following code could get me a valid bearer token I could use to call the Azure REST API.

function Get-AccessToken($tenantId) {
    $azureRmProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile;
    $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azureRmProfile);
    $profileClient.AcquireAccessToken($tenantId).AccessToken;
}

With that in place, we can define a Send-ContainerGroupCommand function that can call any of the start, stop, and restart endpoints for container groups:

function Send-ContainerGroupCommand($resourceGroupName, $containerGroupName, $command) {
    $azContext = Get-AzContext
    $subscriptionId = $azContext.Subscription.Id
    $commandUri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/$containerGroupName/$command" + "?api-version=2018-10-01"
    $accessToken = Get-AccessToken $azContext.Tenant.TenantId
    $response = Invoke-RestMethod -Method Post -Uri $commandUri -Headers @{ Authorization="Bearer $accessToken" }
    $response
}

And then creating our own helper functions to stop and start container groups becomes easy:

function Stop-ContainerGroup($resourceGroupName, $containerGroupName) {
    Send-ContainerGroupCommand $resourceGroupName $containerGroupName  "stop"
}

function Start-ContainerGroup($resourceGroupName, $containerGroupName) {
    Send-ContainerGroupCommand $resourceGroupName $containerGroupName "start"
}

function Restart-ContainerGroup($resourceGroupName, $containerGroupName) {
    Send-ContainerGroupCommand $resourceGroupName $containerGroupName "restart"
}

So now I can stop the Minecraft server we just created with this command:

Stop-ContainerGroup $resourceGroupName $containerGroupName

The great thing about this is that we are no longer paying anything for our container group - they are free while in the stopped state. The only cost is associated with what's in the file share. And when we restart the container group, it will come back up with the same domain name. This is great to allow my children to share the address of the server with their friends, which can remain constant. In fact, I was able to map a custom domain with a DNS CNAME record to the container group's domain name to make the server address even easier to share.

Summary and what's next?

In this post we saw how to fully automate the creation of a Minecraft server running in Azure Container Instances, along with a file share that can persist the server data. But obviously whenever my children want to start the server, they need me to connect to Azure PowerShell and run the start command. And I also need to remember to stop the server at the end of the gaming session or I'll end up with an unexpectedly high bill.

So what would be great next is to automate the process, so I can give my children a way to start the server themselves, that doesn't require them to have access to my Azure subscription, and also a way to automatically shut it down after a certain elapsed duration.

Now that Azure Functions v2 supports PowerShell functions, and integrates directly with the Az Module, it's an obvious choice for the automation. So I'm hoping to follow on with another post soon showing how we can create a PowerShell Azure Functions App to automate the starting and stopping of the Minecraft container.

Want to learn more about how easy it is to get up and running with Azure Container Instances? Be sure to check out my Pluralsight course Azure Container Instances: Getting Started.

0 Comments Posted in:

I've finally got round to releasing NAudio 1.9.0. The big news is that this version targets .NET Standard 2.0, opening the door to NAudio being used on a wider variety of platforms.

Of course, it's important to point out that a large part of NAudio consists of wrappers for various Windows APIs, giving you access to audio input and output devices, as well as taking advantage of audio codecs from ACM or Media Foundation. None of that functionality will work cross-platform, as it's relying on underlying Windows components to do its work.

But there is still a lot of NAudio that is portable to other platforms, so hopefully this will prove useful to people. In addition to targeting .NET Standard 2.0, NAudio still supports .NET 3.5 and also UAP 10.0.240, thanks to the very helpful MSBuild.Sdk.Extras tools from Oren Novotny.

There are a few minor bugfixes included in the latest release. Probably the most notable is that AsioOut no longer auto-stops when it reaches the end of playback. That caused serious problems with many ASIO drivers, so it's disabled by default, although you can restore the original behaviour if you want.

Future of NAudio

I should probably say a few words about the future of NAudio. Although I continue to reply to the many questions asked on GitHub and Stack Overflow every day, you've probably noticed that development hasn't proceeded very rapidly in recent years. That's due to the fact that I simply no longer have the time I used to for maintaining an open source project. My day job is very focused on Azure now, and for the last four years I haven't needed to use NAudio as part of my daily work.

What this means is that NAudio 1.9.0 is not likely to pick up major new features. I'll probably still fix a few bugs from time to time, and when there are pull requests that I am able to easily verify don't break existing functionality (which is not always possible unfortunately), I will also endeavour to accept them into the project as well.

If there were ever to be a version 2 of NAudio, then it would make sense to rethink a number of the core design decisions, in particular to incorporate new language innovations like Span<T> and compiler intrinsics which could bring major performance improvements. I have done a fair bit of experimentation with this (you can see some of my efforts here), and maybe in the future I'll find time to do some more work on that. But unless I find myself actively using NAudio for commercial development again, which doesn't seem likely in the near future, progress is likely to be slow.

So I guess the summary is that although NAudio isn't "dead", it is currently in "maintenance mode". It's a project that means a lot to me - I started it back in 2002 as my first ever open source application, and I'm very glad that it's been of use to so many people over the years. But my focus currently is much more in the Azure space.

If there is anyone out there interested in becoming a maintainer of NAudio, I would be glad to have a conversation with you. And to all those who are still waiting for replies to your questions and requests for help, please accept my apologies - the volume is now too great for me to respond to everything.

I know that several long-running open-source projects with lone maintainers are facing similar issues. Geoff Huntley hosted a fascinating discussion about this problem at the Microsoft MVP Summit earlier this year. And one of my programming heroes, Alexandre Mutel, recently took the decision to retire SharpDX. Maybe there will come a time where I need to do the same, but I'm not completely stepping away just yet.

Want to get up to speed with the the fundamentals principles of digital audio and how to got about writing audio applications with NAudio? Be sure to check out my Pluralsight courses, Digital Audio Fundamentals, and Audio Programming with NAudio.