👾

Enterprise Ready (?) な Minecraft サーバーを構築する

に公開
2025/12/20

変更履歴

  • 2025-12-20: Bicep テンプレート全体と、監視について追記

こんにちわ。
Microsoft Advent Calendar 2025 の 12 月 19 日の記事です🎄

alt text

2年前に Azure 上へ Minecraft サーバーを構築する話をしてました [1]。
前回は VM に展開しただけでセキュリティ面、管理性など考慮が薄かったと思います。
今回は、よりセキュアで、Azure のベストプラクティスにばっちり沿った設定がされている Azure Verified Module (AVM) [2] と呼ばれる Infrastructure-as-Code (IaC) モジュールを使って構築します。

Azure Verified Module とは

はじめに、Azure Verified Module (AVM) とは、Microsoft が主導する Infrastructure-as-Code (IaC) モジュールの標準化プロジェクトです。Bicep と Terraform の両方に対応しており、Azure リソースをデプロイするための再利用可能なモジュールが公式レジストリで公開されています。

このモジュールは Microsoft によって開発・保守されており、Well-Architected Framework [3] の推奨事項に沿った設定がデフォルトで適用されます。これにより、セキュリティやパフォーマンスのベストプラクティスを意識しなくても、ある程度の品質が担保された状態でリソースをデプロイできます。

また、各モジュールは設計、実装、テスト、ドキュメント生成、レビューという一連のプロセス[4]を経て公開されています。その点においてもプロジェクトなどで採用しやすいのではないでしょうか。

Bicep の場合、AVM モジュールは Bicep Public Module Registry で公開されており、br/public:avm/res/<provider>/<resource>:<version> の形式で参照できます。たとえば仮想ネットワークのモジュールであれば br/public:avm/res/network/virtual-network:0.5.2 となります。

なお、Visual Studio Code の Bicep 拡張機能を使用している場合、モジュールは参照時にローカルにキャッシュされます [5]。

WSL2 の場合

Visual Studio Code で使っている場合、カーソルを対象モジュールの箇所に重ねることで、モジュールのドキュメントにジャンプすることもできますので、必須/オプションパラメータの説明やサンプルコードを確認できます。

alt text

アーキテクチャの全体像

それでは今回構築する全容を紹介します。
アーキテクチャとしては、Azure Container Apps 上で Minecraft サーバーを動作させ、インターネットからのアクセスを Azure Firewall で受け付ける構成となっています。

alt text

この構成のポイントは、Container Apps 環境を内部 (internal) モードでデプロイしている点にあります。Container Apps の内部環境は直接インターネットに公開されないため、外部からのアクセスは Azure Firewall の DNAT ルールを経由して受け付けます。これにより、ファイアウォールでトラフィックを一元的に制御しつつ、Minecraft サーバーへの接続を実現しています。

また、ワールドデータを永続化するために Azure Files を使用し、Private Endpoint 経由でセキュアにアクセスする構成としています。

使用する AVM モジュール

この構成では以下の AVM モジュールを使用します。

モジュール バージョン 用途
avm/res/network/virtual-network 0.7.2 仮想ネットワークとサブネットの作成
avm/res/network/private-dns-zone 0.8.0 Storage Account 用のプライベート DNS ゾーン
avm/res/network/public-ip-address 0.10.0 Azure Firewall 用のパブリック IP アドレス
avm/res/network/azure-firewall 0.9.2 Azure Firewall
avm/res/network/firewall-policy 0.3.4 ファイアウォールポリシーとルール
avm/res/operational-insights/workspace 0.9.1 Log Analytics ワークスペース
avm/res/storage/storage-account 0.30.0 Storage Account (Azure Files)
avm/res/app/managed-environment 0.8.1 Container Apps 環境
avm/res/app/container-app 0.12.0 Minecraft コンテナアプリ

仮想ネットワークの構成

仮想ネットワークは 4 つのサブネットで構成されます。

module vnet 'br/public:avm/res/network/virtual-network:0.7.2' = {
  name: '${time}-privateVnet'
  params: {
    name: vnetName
    addressPrefixes: vnetAddressPrefixes
    subnets: [
      {
        name: subnetAzureFirewallName
        addressPrefix: subnetAzureFirewallPrefix
      }
      {
        name: subnetStorageName
        addressPrefix: subnetStoragePrefix
        privateLinkServiceNetworkPolicies: 'Enabled'
        defaultOutboundAccess: false
      }
      {
        name: subnetWebName
        addressPrefix: subnetWebPrefix
        delegation: 'Microsoft.App/environments'
      }
      {
        name: subnetAzureFirewallManagementName
        addressPrefix: subnetAzureFirewallManagementPrefix
      }
    ]
  }
}

AzureFirewallSubnet は Azure Firewall のデータプレーン用のサブネットです。Azure Firewall はこの名前のサブネットを必要とするため、名前を変更することはできません。

storage サブネットは Storage Account の Private Endpoint を配置するためのサブネットです。

app サブネットは Container Apps 環境用のサブネットであり、delegation: 'Microsoft.App/environments' を指定して Container Apps への委任を設定しています。Container Apps のワークロードプロファイル環境では /27 以上のサイズが必要となります。

Azure Firewall とファイアウォールポリシー

Azure Firewall は、仮想ネットワークのトラフィックを制御するためのクラウドネイティブなファイアウォールサービスです。この構成では Standard SKU を使用し、ファイアウォールポリシーでルールを定義しています。

module firewallPolicy 'br/public:avm/res/network/firewall-policy:0.3.4' = {
  name: 'firewallPolicyDeployment'
  params: {
    // Required parameters
    name: azfwpName
    // Non-required parameters
    allowSqlRedirect: true
    snat: {
      autoLearnPrivateRanges: 'Enabled'
    }
    location: location
    enableTelemetry: true
    enableProxy: true
    ruleCollectionGroups: [
      // 省略
    ]
    tier: 'Standard'
  }
}

ファイアウォールポリシーには 2 つのルールコレクショングループを定義しています。

1 つ目の outbound グループでは、Container Apps サブネットからのアウトバウンドトラフィックを制御します。HTTP/HTTPS のアウトバウンドを許可するアプリケーションルールと、Azure のサービスタグ (MicrosoftContainerRegistry、AzureContainerRegistry など) への接続を許可するネットワークルールを設定しています。

2 つ目の minecraft-server グループでは、Minecraft サーバーへのインバウンドトラフィックを制御します。ここで重要なのは DNAT ルールの設定です。

{
  ruleCollectionType: 'FirewallPolicyNatRuleCollection'
  action: {
    type: 'Dnat'
  }
  rules: [
    {
      ruleType: 'NatRule'
      name: 'minecraft-server'
      translatedAddress: managedEnvironment.outputs.staticIp
      translatedPort: '25565'
      ipProtocols: [
        'TCP'
      ]
      sourceAddresses: [
        '*'
      ]
      destinationAddresses: [
        fwpip.outputs.ipAddress
      ]
      destinationPorts: [
        '25565'
      ]
    }
  ]
  name: 'nat-minecraft-server'
  priority: 100
}

DNAT (Destination Network Address Translation) ルールにより、Azure Firewall のパブリック IP アドレス宛の 25565 番ポートへのトラフィックが、Container Apps 環境の静的 IP アドレスに転送されます。なお、25565 は Minecraft Java Edition サーバーのデフォルトポートです。

Storage Account と Private Endpoint

Minecraft サーバーのワールドデータを永続化するために、Azure Files を使用します。Container Apps のコンテナは本来ステートレスであり、再起動時にデータが失われるため、外部ストレージへのデータ保存が必要となります。

module storageAccount 'br/public:avm/res/storage/storage-account:0.30.0' = {
  name: '${time}-storageAccountDeployment'
  params: {
    name: storageAccountName
    fileServices: {
      shares: [
        {
          accessTier: 'Hot'
          name: 'mcjavashare'
          shareQuota: 5120
        }
      ]
    }
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
    }
    privateEndpoints: [
      {
        privateDnsZoneGroup: {
          privateDnsZoneGroupConfigs: [
            {
              privateDnsZoneResourceId: pdnssto.outputs.resourceId
            }
          ]
        }
        service: 'file'
        subnetResourceId: vnet.outputs.subnetResourceIds[1]
      }
    ]
    skuName: 'Standard_ZRS'
  }
}

Storage Account は networkAcls.defaultAction: 'Deny' により、パブリックアクセスをデフォルトで拒否する設定としています。Container Apps からのアクセスは Private Endpoint 経由で行います。

Private Endpoint を作成すると、仮想ネットワーク内からは Storage Account のプライベート IP アドレスでアクセスできるようになります。この名前解決を行うために、プライベート DNS ゾーン privatelink.file.core.windows.net を作成し、仮想ネットワークにリンクしています。

Container Apps 環境

Container Apps 環境は、コンテナアプリを実行するための分離された境界を提供します。この構成では内部モード (internal: true) でデプロイし、インターネットからの直接アクセスを遮断しています。

module managedEnvironment 'br/public:avm/res/app/managed-environment:0.8.1' = {
  name: '${time}-managedEnvironmentDeployment'
  params: {
    // Required parameters
    name: mngEnvName
    // Non-required parameters
    appLogsConfiguration: {
      destination: 'log-analytics'
      logAnalyticsConfiguration: {
        customerId: workspace.outputs.logAnalyticsWorkspaceId
        sharedKey: workspace.outputs.primarySharedKey
      }
    }
    infrastructureSubnetResourceId: vnet.outputs.subnetResourceIds[2]
    internal: true
    location: location
    storages: [
      {
        accessMode: 'ReadWrite'
        storageAccountName: storageAccount.outputs.name
        shareName: 'mcjavashare'
        kind: 'SMB'
      }
    ]
    platformReservedCidr: '172.17.17.0/24'
    platformReservedDnsIP: '172.17.17.17'
    workloadProfiles: [
      {
        maximumCount: 1
        minimumCount: 0
        name: 'CAW01'
        workloadProfileType: 'D4'
      }
    ]
  }
}

storages パラメータで Azure Files 共有をマウントし、Container Apps から利用できるようにしています。kind: 'SMB' は SMB プロトコルでのマウントを意味します。

workloadProfiles では D4 タイプのワークロードプロファイルを定義しています。Minecraft サーバーはある程度のメモリと CPU を必要とするため、従量課金の Consumption プランではなく、Dedicated プランのワークロードプロファイルを使用しています。

Minecraft Container App

最後に、Minecraft サーバー本体の Container App を定義します。

module minecraft 'br/public:avm/res/app/container-app:0.12.0' = {
  name: '${time}-${cappsName}'
  params: {
    name: cappsName
    containers: [
      {
        image: 'docker.io/itzg/minecraft-server'
        name: 'minecraft-server'
        resources: {
          cpu: json('2')
          memory: '4Gi'
        }
        volumeMounts: [
          {
            volumeName: 'mcjavashare'
            mountPath: '/data'
          }
        ]
        env: [
          { name: 'EULA', value: 'true' }
          { name: 'MEMORY', value: '3G' }
          { name: 'OPS', value: 'mattffffff' }
          { name: 'VERSION', value: '1.21.7' }
          { name: 'VIEW_DISTANCE', value: '16' }
        ]
      }
    ]
    volumes: [
      {
        name: 'mcjavashare'
        storageName: 'mcjavashare'
        storageType: 'AzureFile'
      }
    ]
    ingressTargetPort: 25565
    workloadProfileName: 'CAW01'
    environmentResourceId: managedEnvironment.outputs.resourceId
    scaleMinReplicas: 1
    scaleMaxReplicas: 1
    exposedPort: 25565
    ingressTransport: 'tcp'
  }
}

使用しているコンテナイメージは itzg/minecraft-server であり、Docker Hub で公開されている定番の Minecraft サーバーイメージです。環境変数で Minecraft のバージョン、メモリ割り当て、オペレーター権限を持つプレイヤー名などを設定できます。

volumeMounts で Azure Files 共有を /data にマウントしています。Minecraft サーバーのワールドデータや設定ファイルは /data ディレクトリに保存されるため、コンテナが再起動してもデータが永続化されます。

ingressTransport: 'tcp' と exposedPort: 25565 により、TCP 通信を受け付ける設定としています。通常の Container Apps は HTTP/HTTPS のみを受け付けますが、TCP ingress を有効にすることで Minecraft のプロトコルにも対応できます。

Bicep テンプレートの全体はこちら。

Bicep テンプレート
@description('ShortName is required for a unique storage account name. Only 5 characters.')
param shortName string

@description('Address prefixes for the virtual network.')
param vnetAddressPrefixes array = [
  '192.168.1.0/24'
]

@description('Name of the Azure Firewall subnet.')
param subnetAzureFirewallName string = 'AzureFirewallSubnet'

@description('Address prefix for the Azure Firewall subnet.')
param subnetAzureFirewallPrefix string = '192.168.1.0/26'

@description('Name of the storage subnet.')
param subnetStorageName string = 'storage'

@description('Address prefix for the storage subnet.')
param subnetStoragePrefix string = '192.168.1.64/27'

@description('Name of the web subnet.')
param subnetWebName string = 'app'

@description('Address prefix for the web subnet.')
param subnetWebPrefix string = '192.168.1.96/27'

@description('Name of the Azure Firewall management subnet.')
param subnetAzureFirewallManagementName string = 'AzureFirewallManagementSubnet'

@description('Address prefix for the Azure Firewall management subnet.')
param subnetAzureFirewallManagementPrefix string = '192.168.1.128/26'

@description('Private DNS zone name for the storage account.')
param pdnsName string = 'privatelink.file.core.windows.net'

@description('Blob name for the storage account.')
param blobName string = 'mcjavablob'

@description('Location for all resources.')
param location string = resourceGroup().location

@description('Current UTC time, used for resource naming.')
param time string = utcNow()

var mngEnvName = 'cenv-mcjava-${shortName}'
var cappsName = 'capp-mcjava-${shortName}'
var vnetName = 'vnet-mcjava-${shortName}'
var storageAccountName = 'mcjavafiles${shortName}'
var workspaceName = 'law-mcjava-${shortName}'
var cenvpipName = 'cenvpip-mcjava-${shortName}'
var azfwName = 'azfw-mcjava-${shortName}'
var azfwpName = 'azfwp-mcjava-${shortName}'
var azfwpipName = 'azfwpip-mcjava-${shortName}'

// Virtual Network (AVM Module)
module vnet 'br/public:avm/res/network/virtual-network:0.7.2' = {
  name: '${time}-privateVnet'
  params: {
    name: vnetName
    addressPrefixes: vnetAddressPrefixes
    subnets: [
      {
        name: subnetAzureFirewallName
        addressPrefix: subnetAzureFirewallPrefix
      }
      {
        name: subnetStorageName
        addressPrefix: subnetStoragePrefix
        privateLinkServiceNetworkPolicies: 'Enabled'
        defaultOutboundAccess: false
      }
      {
        name: subnetWebName
        addressPrefix: subnetWebPrefix
        delegation: 'Microsoft.App/environments'
      }
      {
        name: subnetAzureFirewallManagementName
        addressPrefix: subnetAzureFirewallManagementPrefix
      }
    ]
  }
}

// Private DNS Zone for Storage Account (AVM Module)
module pdnssto 'br/public:avm/res/network/private-dns-zone:0.8.0' = {
  name: '${time}-storagedns'
  params: {
    name: pdnsName
    virtualNetworkLinks: [
      {
        virtualNetworkResourceId: vnet.outputs.resourceId
      }
    ]
  }
}

// Azure Firewall Public IP (AVM Module)
module fwpip 'br/public:avm/res/network/public-ip-address:0.10.0' = {
  name: '${time}-azfwpipDeployment'
  params: {
    // Required parameters
    name: azfwpipName
    // Non-required parameters

    diagnosticSettings: []
    publicIPAddressVersion: 'IPv4'
    publicIPAllocationMethod: 'Static'
    roleAssignments: []
    skuName: 'Standard'
    skuTier: 'Regional'
  }
}

// Azure Firewall (AVM Module)
module azfw 'br/public:avm/res/network/azure-firewall:0.9.2' = {
  name: '${time}-azureFirewallDeployment'
  params: {
    // Required parameters
    name: azfwName
    azureSkuTier: 'Standard'
    virtualNetworkResourceId: vnet.outputs.resourceId
    location: location
    threatIntelMode: 'Alert'
    firewallPolicyId: firewallPolicy.outputs.resourceId
    publicIPResourceID: fwpip.outputs.resourceId
  }
}

// Firewall Policy (AVM Module)
module firewallPolicy 'br/public:avm/res/network/firewall-policy:0.3.4' = {
  name: 'firewallPolicyDeployment'
  params: {
    // Required parameters
    name: azfwpName
    // Non-required parameters
    allowSqlRedirect: true
    snat: {
      autoLearnPrivateRanges: 'Enabled'
    }
    location: location
    enableTelemetry: true
    enableProxy: true
    ruleCollectionGroups: [
      {
        priority: 1000
        name: 'outbound'
        ruleCollections: [
          {
            ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
            action: {
              type: 'Allow'
            }
            rules: [
              {
                ruleType: 'ApplicationRule'
                name: 'vnet-outbound'
                protocols: [
                  {
                    protocolType: 'Https'
                    port: 443
                  }
                  {
                    protocolType: 'Http'
                    port: 80
                  }
                ]
                fqdnTags: []
                webCategories: []
                targetFqdns: [
                  '*'
                ]
                targetUrls: []
                terminateTLS: false
                sourceAddresses: [
                  subnetWebPrefix
                ]
                destinationAddresses: []
                sourceIpGroups: []
                httpHeadersToInsert: []
              }
            ]
            name: 'vnet-outbound'
            priority: 300
          }
          {
            ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
            action: {
              type: 'Allow'
            }
            rules: [
              {
                ruleType: 'NetworkRule'
                name: 'nrc-containerapp-out'
                ipProtocols: [
                  'TCP'
                  'UDP'
                ]
                sourceAddresses: [
                  subnetWebPrefix
                ]
                sourceIpGroups: []
                destinationAddresses: [
                  'MicrosoftContainerRegistry'
                  'AzureFrontDoorFirstParty'
                  'AzureContainerRegistry'
                  'AzureActiveDirectory'
                  'AzureKeyVault'
                ]
                destinationIpGroups: []
                destinationFqdns: []
                destinationPorts: [
                  '80'
                  '443'
                ]
              }
            ]
            name: 'container-app-outbound'
            priority: 400
          }
        ]
      }
      {
        priority: 1100
        name: 'minecraft-server'
        ruleCollections: [
          {
            ruleCollectionType: 'FirewallPolicyFilterRuleCollection'
            action: {
              type: 'Allow'
            }
            rules: [
              {
                ruleType: 'NetworkRule'
                name: 'nrc-minecraft-server-in'
                ipProtocols: [
                  'TCP'
                ]
                sourceAddresses: [
                  '0.0.0.0/0'
                ]
                sourceIpGroups: []
                destinationAddresses: [
                  managedEnvironment.outputs.staticIp
                ]
                destinationIpGroups: []
                destinationFqdns: []
                destinationPorts: [
                  '25565'
                ]
              }
            ]
            name: 'minecraft-server-in'
            priority: 200
          }
          {
            ruleCollectionType: 'FirewallPolicyNatRuleCollection'
            action: {
              type: 'Dnat'
            }
            rules: [
              {
                ruleType: 'NatRule'
                name: 'minecraft-server'
                translatedAddress: managedEnvironment.outputs.staticIp
                translatedPort: '25565'
                ipProtocols: [
                  'TCP'
                ]
                sourceAddresses: [
                  '*'
                ]
                sourceIpGroups: []
                destinationAddresses: [
                  fwpip.outputs.ipAddress
                ]
                destinationPorts: [
                  '25565'
                ]
              }
            ]
            name: 'nat-minecraft-server'
            priority: 100
          }
        ]
      }
    ]
    tier: 'Standard'
  }
}

// Log Analytics Workspace (AVM Module) with Saved Searches
module workspace 'br/public:avm/res/operational-insights/workspace:0.9.1' = {
  name: '${time}-workspaceDeployment'
  params: {
    // Required parameters
    name: workspaceName
    // Non-required parameters
    location: location
  }
}

// Storage Account (AVM Module)
module storageAccount 'br/public:avm/res/storage/storage-account:0.30.0' = {
  name: '${time}-storageAccountDeployment'
  params: {
    // Required parameters
    name: storageAccountName
    // Non-required parameters
    allowSharedKeyAccess: true
    allowBlobPublicAccess: true
    blobServices: {
      automaticSnapshotPolicyEnabled: true
      containerDeleteRetentionPolicyDays: 10
      containerDeleteRetentionPolicyEnabled: true
      containers: [
        {
          enableNfsV3AllSquash: true
          enableNfsV3RootSquash: true
          name: blobName
          publicAccess: 'None'
        }
      ]
      deleteRetentionPolicyDays: 9
      deleteRetentionPolicyEnabled: true
      diagnosticSettings: [
        {
          metricCategories: [
            {
              category: 'AllMetrics'
            }
          ]
          name: 'customSetting'

          workspaceResourceId: workspace.outputs.resourceId
        }
      ]
      lastAccessTimeTrackingPolicyEnabled: true
    }
    diagnosticSettings: [
      {
        metricCategories: [
          {
            category: 'AllMetrics'
          }
        ]
        name: 'customSetting'

        workspaceResourceId: workspace.outputs.resourceId
      }
    ]
    enableHierarchicalNamespace: true
    enableNfsV3: true
    enableSftp: true
    fileServices: {
      diagnosticSettings: [
        {
          metricCategories: [
            {
              category: 'AllMetrics'
            }
          ]
          name: 'customSetting'
          workspaceResourceId: workspace.outputs.resourceId
        }
      ]
      shares: [
        {
          accessTier: 'Hot'
          name: 'mcjavashare'
          shareQuota: 5120
        }
      ]
    }
    largeFileSharesState: 'Enabled'
    localUsers: []
    location: location
    managedIdentities: {
      systemAssigned: true
    }
    managementPolicyRules: []
    networkAcls: {
      bypass: 'AzureServices'
      defaultAction: 'Deny'
      ipRules: []
      virtualNetworkRules: []
    }
    privateEndpoints: [
      {
        privateDnsZoneGroup: {
          privateDnsZoneGroupConfigs: [
            {
              privateDnsZoneResourceId: pdnssto.outputs.resourceId
            }
          ]
        }
        service: 'file'
        subnetResourceId: vnet.outputs.subnetResourceIds[1]
      }
    ]
    requireInfrastructureEncryption: true
    sasExpirationPeriod: '180.00:00:00'
    skuName: 'Standard_ZRS'
    tags: {
      SecurityControl : 'Ignore'
    }
  }
}

// Container Apps Managed Environment (AVM Module)
module managedEnvironment 'br/public:avm/res/app/managed-environment:0.8.1' = {
  name: '${time}-managedEnvironmentDeployment'
  params: {
    // Required parameters
    name: mngEnvName
    logAnalyticsWorkspaceResourceId: workspace.outputs.resourceId
    // Non-required parameters
    infrastructureSubnetId: vnet.outputs.subnetResourceIds[2]
    internal: true
    location: location
    storages: [
      {
        accessMode: 'ReadWrite'
        storageAccountName: storageAccount.outputs.name
        shareName: 'mcjavashare'
        kind: 'SMB'
      }
    ]
    platformReservedCidr: '172.17.17.0/24'
    platformReservedDnsIP: '172.17.17.17'
    workloadProfiles: [
      {
        maximumCount: 1
        minimumCount: 0
        name: 'CAW01'
        workloadProfileType: 'D4'
      }
    ]
  }
}

// Minecraft Container App (AVM Module)
module minecraft 'br/public:avm/res/app/container-app:0.12.0' = {
  name: '${time}-${cappsName}'
  params: {
    name: cappsName
    containers: [
      {
        image: 'docker.io/itzg/minecraft-server'
        name: 'minecraft-server'
        resources: {
          cpu: json('2')
          memory: '4Gi'
        }
        volumeMounts: [
          {
            volumeName: 'mcjavashare'
            mountPath: '/data'
          }
        ]
        env: [
          { name: 'EULA', value: 'true' }
          { name: 'MEMORY', value: '3G' }
          { name: 'OPS', value: 'mattffffff' }
          { name: 'VERSION', value: '1.21.11' }
          { name: 'VIEW_DISTANCE', value: '16' }
        ]
      }
    ]
    volumes: [
      {
        name: 'mcjavashare'
        storageName: 'mcjavashare'
        storageType: 'AzureFile'
      }
    ]
    ingressTargetPort: 25565
    workloadProfileName: 'CAW01'
    environmentResourceId: managedEnvironment.outputs.resourceId
    scaleMinReplicas: 1
    scaleMaxReplicas: 1
    exposedPort: 25565
    ingressTransport: 'tcp'
    trafficWeight: 100
    trafficLatestRevision: true
  }
}

// Output the Minecraft server endpoint
output minecraftEndpoint string = fwpip.outputs.ipAddress

デプロイと接続

このテンプレートをデプロイすると、出力として Azure Firewall のパブリック IP アドレスが返されます。

output minecraftEndpoint string = fwpip.outputs.ipAddress

Minecraft クライアントからは、このパブリック IP アドレスに対してポート 25565 で接続することでサーバーに参加できます。

alt text

管理

これで構築して遊べるまでにはなりました。
ここからは管理面でのトピックを挙げていきます。

バックアップ

今回の構成では、Minecraft サーバーのワールドデータは Azure Files に保存されます。
サーバーのお引越しが必要な時は、こちらのデータを移行しましょう。

Azure Portal や Azure CLI を使用して手動でバックアップを取得できます。また、Azure Backup サービスを利用して自動バックアップを設定することも可能です。

バージョンアップ

Minecraft は年に1度のバージョンアップから、年に複数回の更新を行う方針に変わったことが昨年発表されました。そのため、バージョンアップは必須となります。

このコンテナイメージでは、Minecraft のバージョンとして、環境変数 VERSION で指定しています。これによって指定されたバージョンで Minecraft サーバーが起動できます。

        env: [
          { name: 'EULA', value: 'true' }
          { name: 'MEMORY', value: '3G' }
          { name: 'OPS', value: 'mattffffff' }
          { name: 'VERSION', value: '1.21.7' }
          { name: 'VIEW_DISTANCE', value: '16' }
        ]

注意点としては、Bicep テンプレートの対象環境変数を更新して、再度 Bicep テンプレートを流せばよいのですが、前のリビジョンでファイル共有をつかんでいる関係で、新規リビジョンのコンテナで Minecraft サーバーの起動が失敗します。

alt text

その場合、一度 Container App を停止、その後起動することで正常にリビジョンが反映されるかと思います。

監視

Log Analytics ワークスペースを Container Apps 環境に紐づけることで、コンテナのアプリケーションにかかわるログを転送できます。

このログには、Minecraft サーバーへログインしたユーザーや、ゲーム内で入力したチャットメッセージが含まれます。たとえば、以下の KQL クエリを使用して、Minecraft サーバーの起動が完了した際のメッセージを取得できます。

ContainerAppConsoleLogs_CL
| where Log_s has "0.0.0.0"

alt text

また、Azure Monitor に含まれる Grafana ダッシュボードが用意されています。
そしてなんと無料。ざっとパフォーマンスの傾向を見るのに便利ですね。
Minecraft サーバーのパフォーマンス戦略としてはスケールアップになるので、傾向をつかんで早めに対処できるようになりましょう。

alt text

IaC リソースのメンテナンス

AVM は継続的にアップデートされているため、新しいバージョンがリリースされた際にはテンプレートのバージョン番号を更新することで、最新の機能やセキュリティ改善を取り込めます。Bicep の bicepconfig.json で use-recent-module-versions を有効にすると、古いバージョンのモジュールを使用している場合に警告が表示されるため、アップデートの見落としを防ぐことができます [6]。

まとめ

いかがでしたでしょうか⛏️

本記事では、Azure Verified Module を使って Minecraft サーバーのインフラストラクチャを構築する方法を解説しました。AVM を使用することで、Microsoft が推奨する設定が適用された状態でデプロイできます。また、一から作るよりも開発効率が大幅に向上します。

ぜひ、Azure での構築を検討される際には、まず AVM に対応するモジュールがないか確認してみてください。

脚注
  1. 自宅マイクラサーバ をAzureに移行 ↩︎

  2. Azure Verified Modules documentation ↩︎

  3. Azure Well-Architected Framework ↩︎

  4. Bicep Contribution Flow ↩︎

  5. Using modules in Bicep ↩︎

  6. リンター ルール - 最新のモジュール バージョンを使用する ↩︎

GitHubで編集を提案

Discussion