We will build and upload a game server in the final part of this article series. The clients we implemented in previous posts will connect to this server, and we will have a simple but functional web multiplayer game. We will only use free services.
Building Server Infrastructure with Terraform
First, we will extend our Terraform script (terraform.yml), by adding a virtual machine, placing it in a virtual network, a subnet, and assigning a public IP to it:
resource "azurerm_virtual_network" "this" {
name = "mygame-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
}
resource "azurerm_subnet" "this" {
name = "internal"
resource_group_name = azurerm_resource_group.this.name
virtual_network_name = azurerm_virtual_network.this.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_public_ip" "this" {
name = "mygame-pip"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
allocation_method = "Dynamic"
}
resource "azurerm_network_interface" "this" {
name = "mygame-nic"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
ip_configuration {
name = "mygame-publicip"
subnet_id = azurerm_subnet.this.id
public_ip_address_id = azurerm_public_ip.this.id
private_ip_address_allocation = "Dynamic"
domain_name_label = "mygame"
}
}
resource "azurerm_linux_virtual_machine" "this" {
name = "mygame-vm"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
size = "Standard_B1s"
admin_username = "mygamevmuser"
admin_password = "GROI43gdfVCB+!1"
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.this.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
}
Code language: JavaScript (javascript)
Now commit your new code, push it to GitHub, and execute the Terraform workflow.
Build Your Game Server
Because our game code is already in the GitHub repository, it is extremely simple to build a game server. We simply need to create a new workflow (let’s call it BuildServer.yml) that builds our game with a custom parameter (EnableHeadlessMode). Otherwise, the steps are the same as it was at the game client:
name: Build Linux Server
on:
workflow_dispatch: {}
jobs:
buildServerForLinux:
name: Linux Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
lfs: true
- name: Cache
uses: actions/cache@v2
with:
path: Library
key: Library-Linux
- name: Build
uses: game-ci/unity-builder@v2
with:
customParameters: -EnableHeadlessMode
targetPlatform: StandaloneLinux64
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
- name: Upload
uses: actions/upload-artifact@v2
with:
name: mygameserver
path: build/StandaloneLinux64
Code language: JavaScript (javascript)
Deploy Your Game Server
We currently have a virtual machine in Azure and a game server built in GitHub. Our last task is to transfer this game server to the virtual machine. You can also place the deploying steps immediately after the build job. Then you have an end-to-end workflow. Let us now create a separate deploying workflow. Make a new file (./github/workflows/DeployServer.yml) and paste in the following code:
name: Deploy Linux Server
on:
workflow_dispatch: {}
jobs:
deployClientToAppService:
name: Deploy Server to Azure VM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: BuildServer.yml
workflow_conclusion: success
name: mygameserver
path: build/StandaloneLinux64
- name: copy file
uses: canastro/copy-file-action@master
with:
source: "Service/mygameserver.service"
target: "build/"
- name: copy file via ssh password
uses: appleboy/scp-action@master
with:
host: ${{ secrets.AZURE_SERVER_HOST }}
username: ${{ secrets.AZURE_SERVER_USERNAME }}
password: ${{ secrets.AZURE_SERVER_PASSWORD }}
source: "build/"
target: "/home/mygamevmuser/mygameserver"
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AZURE_SERVER_HOST }}
username: ${{ secrets.AZURE_SERVER_USERNAME }}
password: ${{ secrets.AZURE_SERVER_PASSWORD }}
script: |
chmod 755 /home/mygamevmuser/mygameserver/build/StandaloneLinux64/StandaloneLinux64
ls -l /home/mygamevmuser/mygameserver/build/
sudo mv /home/mygamevmuser/mygameserver/build/mygameserver.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable mygameservice
sudo systemctl start mygameservice
sudo systemctl status mygameservice
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: restart VM
run: az vm restart -g mygame-resources -n mygame-vm --no-wait
Code language: JavaScript (javascript)
To download the built artifact from the build workflow, we will make use of a special GitHub Action (dawidd6/action-download-artifact@v2). If the build and deploying are in the same workflow, we could use the standard GitHub upload/download method.
The virtual machine should start our game server build at every startup. We can achieve that by creating a service (Service/mygame.service) with the following content:
[Unit]
Description=MyGame Server
[Service]
Type=simple
ExecStart=/home/mygamevmuser/mygameserver/build/StandaloneLinux64/StandaloneLinux64
[Install]
WantedBy=multi-user.target
Code language: JavaScript (javascript)
Commit and push this file to our repository, and copy this file by our deployment workflow with SCP to the virtual machine. For that, set some GitHub secrets. One of them is the AZURE_SERVER_HOST, which is your public IP:
az vm show -d -g mygame-resources -n mygame-vm --query publicIps -o tsv
The AZURE_SERVER_USERNAME and AZURE_SERVER_PASSWORD you can copy directly from the Terraform script.
You can copy the AZURE_SERVER_USERNAME and AZURE_SERVER_PASSWORD directly from the Terraform script, then we only need to start and enable this game server service, for it to run any time the virtual machine starts up.
To restart the virtual machine, we have to use Azure CLI. To achieve that, let’s set the AZURE_CREDENTIALS secret. We need to create a service principal and grant at least a contributor role on our subscription:
az account show --query=id
az ad sp create-for-rbac --name "github-action" --role contributor --scopes /subscriptions/<subscription_id> --sdk-auth
{
"clientId": "<clientId>",
"clientSecret": "<clientSecret>",
"subscriptionId": "<subscriptionId>",
"tenantId": "<tenantId>",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
"resourceManagerEndpointUrl": "https://management.azure.com/",
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
"galleryEndpointUrl": "https://gallery.azure.com/",
"managementEndpointUrl": "https://management.core.windows.net/"
}
Code language: PHP (php)
Copy the result of the above command to the AZURE_CREDENTIALS secret in GitHub.
Commit and push your new workflow to the GitHub repository and run the workflow manually. The server is ready to accept client connections.
Putting it All Together
Start again your application from your browser:
az webapp config hostname list --webapp-name mygame-app-service -g mygame-resources --query=[0].name
"mygame-app-service.azurewebsites.net"
Code language: PHP (php)
If you try to start it from another browser window, you should be able to join the game as another player. The movement of each player is also visible to all other players.
Conclusion
In this article, we created a virtual machine in Azure to host our game server. We built and deployed the game server with the help of GitHub Actions. This simple setup shows an end-to-end solution for a multiplayer web game. Throughout the setup, we made use of only free services, which suits well for learning the general idea.
I purposefully avoided topics like security (don’t leave your VM’s ssh port open), scalability, performance, and a slew of other non-functional concerns. Your real published game should make use of paid services and incorporate those features. I may address them in a subsequent or later post.