Эта статья НЕ про майнкрафт. Эта статья про 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

image

Данные будем хранить отдельно от виртуальной машины на отдельном 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

initial_preparation.sh
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 — папка с начальной картой

prepare_distr.sh
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 файле и в скриптах формирования архива с конфигурацией указаны и совпадают.

И тут мы опять следуем требованиям очередного правила конфигурационного управления:

Все зависимости должны быть точно определены и ресолвиться всегда однозначно

prepare_config.sh
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:

iaas_preparation.sh

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 до архива с конфигурацией и значения параметров

server_configuration.sh
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 = "$mineHome\server.properties"
   Type = "File"
   Ensure = "Present"
   Force = $true
   Contents = "....."
}

File CheckEULA
{
   DestinationPath = "$mineHome\eula.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 = "$mineHome\initial_world"
   MatchSource = $false
   DependsOn = "[xWaitForVolume]WaitForF"    
}

Декларируем о том, что на диске F должна присутствовать директори world (с миром). Если её там нет — мы считаем что происходит деплой впервые и нам надо эту папку создать, взяв за основу карту из initial_world дистрибутива:

Script LinkWorldDirectory
{
   DependsOn="[File]WorldDirectoryExists"
   GetScript=
   {
      @{ Result =  (Test-Path "$using:mineHome\World") }
   }
   SetScript=
   {
      New-Item -ItemType SymbolicLink -Path "$using:mineHome\World" -Confirm -Force -Value "F:\world"                
   }
   TestScript=
  {
      return (Test-Path "$using:mineHome\World")
   }
}

Это кастомный шаг — проверяем, что у нас в папке с сервером есть папка World. Если её нету — линкуем её с диска F. Script работает следующим образом — если TestScript вернул false — вызывается SetScript. Иначе не вызывается:

Script EnsureServerStart
{
   DependsOn="[Script]LinkWorldDirectory"
   GetScript=
   {
      @{ Result = (Get-Process -Name java -ErrorAction SilentlyContinue) } 
   }
   SetScript=
   {
      Start-Process -FilePath "$using:mineHome\jre\bin\java" -WorkingDirectory "$using:mineHome" -ArgumentList "-Xms512M -Xmx512M  -jar `"$using:mineHome\minecraft_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 (очень интересные сами по себе) — освещу их в следующей.

Всем спасибо за внимание.

Как всё это запустить из Windows
Нам нужен будет


Запуск процедуры ролаута

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
 

Результат:

image

Комментарии (6)


  1. kovserg
    01.10.2017 12:09
    +1

    А причем тут DevOps?


    1. Dronopotamus Автор
      01.10.2017 21:04

      а какое определение девопс в Вашей методичке? :)


      1. kovserg
        02.10.2017 00:47

        Очередная модная мантра. Много шума и тумана почти новая религия. Но по содержанию ниочем.
        "… Считается, что движение DevOps зародилось в 2009 году..."


  1. fsou11
    01.10.2017 12:40

    Подскажите стоимость вашего решения с учетом самой базовой подписки и как она будет коррелировать с количеством игроков?


    1. Dronopotamus Автор
      01.10.2017 21:47
      +1

      Тема оптимизации затрат достойна отдельной статьи. Сейчас там создаётся избыточно мощная VM (1 core + 3.5 GB памяти), которая стоит около 3600 в месяц. В любом случае, я лично ничего не трачу, т.к. к visual studio enterprise каждому разработчику прилагается ежемесячный кредит в 150$ на азуру — я использую его. Хватает на все эксперименты.


      1. fsou11
        02.10.2017 11:02

        Не знал что кредит ежемесячный. Интересно, поэкспериментирую. Спасибо.