Running a Minecraft Server in Azure Container Instances
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.
Comments
Thanks for the article. My kids love Minecraft as well. As far as automating the process you may want to look at logic apps. You could easily do this on a schedule or creating a secret link that would start this when visited.
Clint DavisYes, there are plenty of ways to do it - I've already blogged a few times about using C# functions to automate ACI containers, but I was looking for a good excuse to give the new Azure Functions PowerShell a try and this seemed like a good opportunity! I'm planning to have a secret link to start the container, and a scheduled message to stop it.
Mark HeathNice
Jagadeesh WaranHi Mark, thank for the article, still valid one year after (and under lockdown).
raffaeleJust to implement the starting and stopping of the container:
Invoke-AzResourceAction -ResourceGroupName $resourceGroupName -ResourceName $containerGroupName -Action Stop -ResourceType Microsoft.ContainerInstance/containerGroups
courtesy of https://stackoverflow.com/a/52601071/2411043
thanks, that's really helpful
Mark HeathHi Mark, Thank you so much for the guide. It's great and still works.
Jake SmillieI'm new to minecraft and wanted to setup a server for my son and friends. They use their kindle fires so i think they have the PE / Bedrock edition?
I followed your guide and changed the image to be itzg/minecraft-bedrock-server and port to 19132
The server says it is started in the logs console but when i try to connect on the kindle or a windows 10 client i just get "Unable to Connect to World".
Maybe i am getting my bedrocks mixed up and I cant use that in this instance. Any ideas?
I think it might be because the bedrock port needs to be UDP rather than TCP. I cant for the life of me find out how to use the Powershell CLI to create a container with UDP
Jake SmillieNo, I believe Minecraft bedrock can't connect to that server, it has to be the Java edition of Minecraft
Mark HeathCool, thanks Mark. Think I've confirmed it is the UDP, issue by creating a container manually through the azure portal (which gives the UDP option) - this then works. I will see if I can find a way to set this via power shell so I can also setup the correct storage like yours. Thinking about using the a JSON config file maybe.
Jake SmillieManaged to get it working with the work around of creating the server using a JSON resource file. Weird there doesn't seem to be a way of enable a UDP port with CLI.
Jake SmillieNot to worry, thanks very much. I have a very happy son now!
Still all works in 2021, too! :) Has one setup the Minecraft Console to connect to the ACI instance? Can't seem to run it in the Azure Portal, and wondering the best method.
Ed B.For those coming here and trying to provision a Bedrock Edition container image, the Az Powershell Module does not support setting the port protocol to UDP that is required for Bedrock servers. The following Azure CLI command worked for me:
Paul Upsonaz container create `
--resource-group $resourceGroupName `
--name $containerGroupName `
--image itzg/minecraft-bedrock-server `
--dns-name-label $dnsNameLabel `
--ports 19132 `
--protocol UDP `
--azure-file-volume-account-name $storeageAccountName `
--azure-file-volume-account-key $storageAccountKey `
--azure-file-volume-share-name $shareName `
--azure-file-volume-mount-path /data `
--environment-variables EULA=TRUE `
GAMEMODE=creative `
DIFFICULTY=peaceful
Echoing the thanks to Mark for this...but i was able to get to the Minecraft Console by first executing a remote command to invoke a bash shell, and then running 'rcon-cli' from there. I was at least able to then change the weather and time of day in MC.
Stephen Spencefor reference: https://docs.microsoft.com/...
Any idea what the issue might be because of?
Chris MNew-AzContainerGroup:
Line |
3 | -Image "itzg/minecraft-server" `
| ~~~~~~~~~~~~~~~~~~~~~~~
| Cannot process argument transformation on parameter 'ImageRegistryCredential'. Cannot convert value "itzg/minecraft-server" to type "Microsoft.Azure.PowerShell.Cmdlets.ContainerInstance.Models.Api20210301.IImageRegistryCredential[]". Error: "Cannot convert the "itzg/minecraft-server" value of type "System.String" to type "Microsoft.Azure.PowerShell.Cmdlets.ContainerInstance.Models.Api20210301.IImageRegistryCredential"."
It's trying to show the word "ImageRegistryCredential" at the end of the line.
Chris MAzure has made some changes since this post :) Try this:
David Pratt$containerGroupName = "minecraft20190514-cg"
$containerName = "minecraft20190514"
$dnsNameLabel = "minecrafttest"
$volume = New-AzContainerGroupVolumeObject -Name "data" -AzureFileShareName $shareName -AzureFileStorageAccountName $storageAccountCredentials.UserName -AzureFileStorageAccountKey $storageAccountCredentials.Password
$volumeMount = New-AzContainerInstanceVolumeMountObject -Name "data" -MountPath "/data"
$port = New-AzContainerInstancePortObject -Port 25565 -Protocol TCP
$env1 = New-AzContainerInstanceEnvironmentVariableObject -Name "EULA" -Value "TRUE"
$env2 = New-AzContainerInstanceEnvironmentVariableObject -Name "OPS" -Value "YourMinecraftUserName"
$container = New-AzContainerInstanceObject `
-Name $containerGroupName `
-Image "itzg/minecraft-server" `
-VolumeMount $volumeMount `
-Port @($port) `
-EnvironmentVariable @($env1, $env2)
New-AzContainerGroup `
-ResourceGroupName $resourceGroupName `
-Name $containerGroupName `
-Location $location `
-Volume $volume `
-Container $container `
-IpAddressType Public `
-OsType Linux `
-IPAddressDnsNameLabel $dnsNameLabel