Эта статья НЕ про майнкрафт. Эта статья про Azure и про современные подходы к деливери ПО.
Если вы просто хотите развернуть себе сервер майнкрафт — спросите гугл «minecraft server hosting» — будет и проще и дешевле.
А вот если вы хотите посмотреть на реальный кейс использования подходов Infrastructure as Code, Configuration as Code применительно к неадаптированному к Azure ПО на примере майнкрафта — то добро пожаловать
Roadmap
Azure Stack огромен и хочется потыкать в разные его части, поэтому эта статья будет всяко не одна.
План статей пока видится такой:
- Автоматизированный деплой и настройка сервера с применением Azure CLI 2 и Powershell DSC
- Поддержка конфигурации сервера в консистентном состоянии с применением Azure Automation
- Автоматизированное обновление сервера на новую версию
- Получение логов майнкрафта, анализ и отображение с применением Azure Stream Analytics и Power BI
- Организация active/passive кластера с управлением через Azure Automation
- ...
Но конечно процесс появления новых статей может быть остановлен отсутствием вашего интереса )
Disclaimer
- Я не it инженер, не devops инженер, не игрок в майнкрафт. Поэтому если вы шарите и вам кажется, что я всё делаю не правильно — то скорее всего так оно и есть — напишите в коментах как надо.
- Все примеры к статье 100% рабочие, но поставляются «как есть». Если у вас что-то не работает — почините это самостоятельно или забейте.
Итак.
Автоматизированный деплой и настройка сервера с применением Azure CLI 2 и Powershell DSC
О задаче
Задача не синтетическая. Сервер попросили развернуть дети и он реально будет использоваться. Меньше всего я хочу его постоянно чинить и поднимать. Ещё меньше я хочу терять данные при обновлениях. Поэтому требования к нему вполне себе prod-like )
Общий подход
- 100% автоматизация.
- Относимся к скриптам как к коду
- Соблюдаем базовые требования конфигурационного управления к деливери процессу на prod окружения
Все пункты буду пояснять по ходу дела
О ПО
В качестве сервера я взял официальный сервер minecraft.net/en-us/download/server. Беглое изучение выявило, что:
- Сервер представляет из себя jar файл и требует для запуска java
- Для запуска ему требуется рядом с собой файл eula.txt с определённым содержимым
- Сервер конфигурируется с помощью файла server.properties который тоже должен лежать рядом
- Сервер слушает 25565 порт по протоколу TCP
- Данные (карту) сервер хранит в папочке World рядом с собой (и это будет слегка проблемой)
- Сервер умеет данные апгрейдить, если они созданы предыдущей версией (и это сильно упростит жизнь при апгрейде)
- Логи хранятся в папочке Logs рядом
Инфраструктура
Применим самую базовую схему:
одна Resource Group c:
- Network Security Group (будут открыты порты 25565 (minecraft) и 3389 (rdp)
- VNET c 1 subnet
- NIC и статический public IP c DNS именем
- 1 VM под Windows Server 2016 core + 2 Managed Disk (OS + Data)
- Storage account и 1 container для Blob storage
Данные будем хранить отдельно от виртуальной машины на отдельном managed disk и линковать в папочку World рядом с jar'кой с помощью Symlink (я писал выше, что расположение папочки world не настраивается и должно быть всегда рядом с jar).
Это даст возможность запускать сервер как с прилинкованным диском, так и без (для тестов) — в этом случае папка world будет создана с пустой картой.
Сервер будем конфигурировать на основании:
- Дистрибутива (jar с сервером + jre + начальные данные), который будет предварительно загружаться в Blob storage
- Конфигурации (Powershell DSC), которая также будет предварительно загружаться в Blob storage
Я там выше обещал следования некоторым требованиям конфигурационного управления — вот первое:
все артефакты, которые требуются для разворачивания приложения, должны быть опубликованы в подконтрольный сторадж с высокой доступностью.
Именно поэтому мы и дистрибутив и конфигурацию будем предварительно собирать из артефактов в интернетах и перепубликовывать в Blob Storage.
Таким образом, алгоритм процедуры развертывания следующий:
- Создать новую Resource Group, Storage Account и Storage Container
- Сформировать дистрибутив (jar с сервером + jre + начальные данные) и загрузить его в Blob Storage
- Создать всю остальную инфраструктуру в Resource Group
- Сформировать DSC конфигурацию и загрузить её в Blob Storage
- Сконфигурировать сервер на основании конфигурации и дистрибутива
Исходники
- Храним на github
- Соблюдаем какую-нибудь внятную стратегию бранчевания и релизного цикла (ну например gitflow и релиз каждую статью :))
- Используем azure cli 2 и bash там, где это возможно (к слову, в этой статье удалось совсем избежать использования powershell api. А вот в следующей это уже не получится)
Реализация шагов процедуры развертывания
1. Создать новую Resource Group, Storage Account и Storage Container
реализация тривиальная, просто вызовы cli
az configure --defaults location=$LOCATION group=$GROUP
echo "create new group"
az group create -n $GROUP
echo "create storage account"
az storage account create -n $STORAGE_ACCOUNT --sku Standard_LRS
STORAGE_CS=$(az storage account show-connection-string -n $STORAGE_ACCOUNT)
export AZURE_STORAGE_CONNECTION_STRING="$STORAGE_CS"
echo "create storage container"
az storage container create -n $STORAGE_CONTAINER --public-access blob
2. Сформировать дистрибутив (jar с сервером + jre + начальные данные) и загрузить его в Blob Storage
Скачиваем сервер и карту, jre берём с текущего компьютера и всё пакуем в zip
структура дистрибутива:
- .jar
- jre
- initial_world — папка с начальной картой
mkdir $DISTR_DIR
cd $DISTR_DIR
echo "download minecraft server from official site"
curl -Os https://s3.amazonaws.com/Minecraft.Download/versions/1.12.2/minecraft_server.$MVERSION.jar
echo "copy jre from this machine"
cp -r "$JRE" ./jre
echo "create ititial world folder"
mkdir initial_world
cd initial_world
echo "download initial map"
curl -Os https://dl01.mcworldmap.com/user/1821/world2.zip
unzip -q world2.zip
cp -r StarWars/* .
rm -r -f StarWars
rm world2.zip
cd ../
echo "create archive (zip utility -> https://ranxing.wordpress.com/2016/12/13/add-zip-into-git-bash-on-windows/)"
cd ../
zip -r -q $DISTR_ZIP $DISTR_DIR
rm -r -f $DISTR_DIR
3. Сформировать DSC конфигурацию и загрузить её в Blob Storage
DSC конфигурация представляет собой один ps1 файл (о структуре его — позже), который будет запускаться на сервере и его конфигурировать.
Но скрипты в ps1 файле имеют депенды на внешние модули, которые не будут установлены на сервер автоматически.
Поэтому конфигурацию надо передать на сервер не просто как ps1 файл, а как архив с:
- ps1 файлом
- Всеми зависимыми модулями, сложенными в одноименных папочках рядом с ps1 файлом
Зависимые модули можно просто скачать curl'ом как nuget пакет с репозитория powershell gallery и разархивировать в нужное место.
Также можно использовать и команду powershell Save-Module — которая включает эти два шага
Обратите внимание, что версии зависимых модулей и в ps1 файле и в скриптах формирования архива с конфигурацией указаны и совпадают.
И тут мы опять следуем требованиям очередного правила конфигурационного управления:
Все зависимости должны быть точно определены и ресолвиться всегда однозначно
echo "prepare server configuration"
curl -s -L -o configuration/xPSDesiredStateConfiguration.zip "https://www.powershellgallery.com/api/v2/package/xPSDesiredStateConfiguration/7.0.0.0"
curl -s -L -o configuration/xNetworking.zip "https://www.powershellgallery.com/api/v2/package/xNetworking/5.1.0.0"
curl -s -L -o configuration/xStorage.zip "https://www.powershellgallery.com/api/v2/package/xStorage/3.2.0.0"
cd configuration
unzip -q xPSDesiredStateConfiguration.zip -d xPSDesiredStateConfiguration
rm -r xPSDesiredStateConfiguration.zip
unzip -q xNetworking.zip -d xNetworking
rm -r xNetworking.zip
unzip -q xStorage.zip -d xStorage
rm -r xStorage.zip
zip -r -q ../$CONFIG_ZIP . *
rm -r -f xPSDesiredStateConfiguration
rm -r -f xNetworking
rm -r -f xStorage
cd ../
4. Создать всю остальную инфраструктуру в Resource Group
реализация тривиальная, просто вызовы cli:
echo "create network security group and rules"
az network nsg create -n $NSG
az network nsg rule create --nsg-name $NSG -n AllowMinecraft --destination-port-ranges 25556 --protocol Tcp --priority 100
az network nsg rule create --nsg-name $NSG -n AllowRDP --destination-port-ranges 3389 --protocol Tcp --priority 110
echo "create vnet, nic and pip"
NIC_NAME=minesrvnic
PIP_NAME=minepip
SUBNET_NAME=servers
az network vnet create -n $VNET --subnet-name $SUBNET_NAME
az network public-ip create -n $PIP_NAME --dns-name $DNS --allocation-method Static
az network nic create --vnet-name $VNET --subnet $SUBNET_NAME --public-ip-address $PIP_NAME -n $NIC_NAME
echo "create data disk"
DISK_NAME=minedata
az disk create -n $DISK_NAME --size-gb 10 --sku Standard_LRS
echo "create server vm"
az vm create -n $VM_NAME --size $VM_SIZE --image $VM_IMAGE
--nics $NIC_NAME
--admin-username $VM_ADMIN_LOGIN --admin-password $VM_ADMIN_PASSWORD
--os-disk-name ${VM_NAME}disk --attach-data-disk $DISK_NAME
5. Сконфигурировать сервер на основании конфигурации и дистрибутива
Вот тут, наверное самое интересное место. Конфигурирование сервера происходит на основании DSC конфигурации, загруженной ранее с помощью DSC расширения для vm.
DSC extension кормится конфигом в котором указан URL до архива с конфигурацией и значения параметров
echo "prepare dsc extension settings"
cat MinecraftServerDSCSettings.json | envsubst > ThisMinecraftServerDSCSettings.json
echo "configure vm"
az vm extension set
--name DSC
--publisher Microsoft.Powershell
--version 2.7
--vm-name $VM_NAME
--resource-group $GROUP
--settings ThisMinecraftServerDSCSettings.json
rm -f ThisMinecraftServerDSCSettings.json
О Powershell DSC
Ну это некоторая надстройка над powershell, которая позволяет декларативно описывать требуемое состояние компьютера.
DSC конфигурация анализируется на целевой машине, строится диф относительно текущей конфигурации этой машины, а затем происходит приведение конфигурации к нужной. Ближайший аналог powershell DSC в «opensource» мире — Ansible.
Реализация DSC конфигурации MinecraftServer
Структурно конфигурация состоит из набора шагов, которые выполняются в определенном порядке (с учётом зависимостей). Пробежимся по всем:
xRemoteFile DistrCopy
{
Uri = "https://$accountName.blob.core.windows.net/$containerName/mineserver.$minecraftVersion.zip"
DestinationPath = "$mineHome.zip"
MatchSource = $true
}
Декларирует, что на компьютере должен лежать zip с дистрибутивом сервера по пути DestinationPath, если его там не лежит — он будет скачен по адресу Uri:
Archive UnzipServer
{
Ensure = "Present"
Path = "$mineHome.zip"
Destination = $mineHomeRoot
DependsOn = "[xRemoteFile]DistrCopy"
Validate = $true
Force = $true
}
Декларирует, что на компьютере в папке Destination должен лежить разархивированный архив с дистрибутивом сервера если его там не лежит (или если состав файлов отличается от того что в архиве) — они будут добавлены:
File CheckProperties
{
DestinationPath = "$mineHomeserver.properties"
Type = "File"
Ensure = "Present"
Force = $true
Contents = "....."
}
File CheckEULA
{
DestinationPath = "$mineHomeeula.txt"
Type = "File"
Ensure = "Present"
Force = $true
Contents = "..."
DependsOn = "[File]CheckProperties"
}
Декларирует, что по пути DestinationPath должны лежать файлы с конфигом сервера и с лицензией. Если их нет — они создаются с содержимым Contents:
xWaitForDisk WaitWorldDisk
{
DiskIdType = "Number"
DiskId = "2"
RetryIntervalSec = 60
RetryCount = 5
DependsOn = "[File]CheckEULA"
}
xDisk PrepareWorldDisk
{
DependsOn = "[xWaitForDisk]WaitWorldDisk"
DiskIdType = "Number"
DiskId = "2"
DriveLetter = "F"
AllowDestructive = $false
}
xWaitForVolume WaitForF
{
DriveLetter = 'F'
RetryIntervalSec = 5
RetryCount = 10
DependsOn = "[xDisk]PrepareWorldDisk"
}
Тут мы декларируем о том, что managed disk с данными должен быть инициализирован в OS и ему должна быть присвоена буква F:
File WorldDirectoryExists
{
Ensure = "Present"
Type = "Directory"
Recurse = $true
DestinationPath = "F:world"
SourcePath = "$mineHomeinitial_world"
MatchSource = $false
DependsOn = "[xWaitForVolume]WaitForF"
}
Декларируем о том, что на диске F должна присутствовать директори world (с миром). Если её там нет — мы считаем что происходит деплой впервые и нам надо эту папку создать, взяв за основу карту из initial_world дистрибутива:
Script LinkWorldDirectory
{
DependsOn="[File]WorldDirectoryExists"
GetScript=
{
@{ Result = (Test-Path "$using:mineHomeWorld") }
}
SetScript=
{
New-Item -ItemType SymbolicLink -Path "$using:mineHomeWorld" -Confirm -Force -Value "F:world"
}
TestScript=
{
return (Test-Path "$using:mineHomeWorld")
}
}
Это кастомный шаг — проверяем, что у нас в папке с сервером есть папка World. Если её нету — линкуем её с диска F. Script работает следующим образом — если TestScript вернул false — вызывается SetScript. Иначе не вызывается:
Script EnsureServerStart
{
DependsOn="[Script]LinkWorldDirectory"
GetScript=
{
@{ Result = (Get-Process -Name java -ErrorAction SilentlyContinue) }
}
SetScript=
{
Start-Process -FilePath "$using:mineHomejrebinjava" -WorkingDirectory "$using:mineHome" -ArgumentList "-Xms512M -Xmx512M -jar `"$using:mineHomeminecraft_server.$using:minecraftVersion.jar`" nogui"
}
TestScript=
{
return (Get-Process -Name java -ErrorAction SilentlyContinue) -ne $null
}
}
Ещё один кастомный шаг. Проверяем, что запущен java процесс с сервером ). Если не запущен — запускаем:
xFirewall FirewallIn
{
Name = "Minecraft-in"
Action = "Allow"
LocalPort = ('25565')
Protocol = 'TCP'
Direction = 'Inbound'
}
xFirewall FirewallOut
{
Name = "Minecraft-out"
Action = "Allow"
LocalPort = ('25565')
Protocol = 'TCP'
Direction = 'Outbound'
}
И последние шаги — открываем 25565 порт.
Эпилог
Статья получилась и так длинная, надо закругляться. Остались за бортом вопросы дебага этого DSC (очень интересные сами по себе) — освещу их в следующей.
Всем спасибо за внимание.
Запуск процедуры ролаута
git clone https://github.com/AndreyPoturaev/minecraft-in-azure
cd minecraft-in-azure
git checkout v1.0.0
export MINESERVER_PASSWORD=<place your password here>
export MINESERVER_DNS=<place unique name here. Server url will be $MINESERVER_DNS.westeurope.cloudapp.azure.com>
export MINESERVER_STORAGE_ACCOUNT=<place unique name here>
az login
. rollout.sh
Результат:
Автор: Потураев Андрей