Jetzt, da wir eine VM haben, die eine Website bereitstellt, ist es ein gängiges Muster, nicht nur eine VM bereitzustellen. Mehrere VMs werden genutzt, um die Last zu verteilen. In Azure heißt diese Funktion Virtual Machine Scale Set (siehe die Dokumentation).

Um dies in Terraform umzusetzen, benötigen wir den Ressourcentyp azurerm_linux_virtual_machine_scale_set. Die Dokumentation zeigt ein Beispiel zur Verwendung.

Bitte zuerst lesen!

ACHTUNG - Ich habe alles mehrfach durchgeführt und viele mögliche Parameter ausprobiert, um das Scale Set inklusive Apache-Webserver bereitzustellen. Ich habe nicht herausgefunden, warum die Konfiguration der Custom Script Extension beim initialen Deployment nicht funktioniert. Nur wenn man die VM-Anzahl nach dem Deployment ändert, wird das Custom Script bereitgestellt. Dieses Problem kann man hier sehen.

Ich gehe das komplette Beispiel durch und möchte danach zeigen, wie ich das Beispiel aus Yevgeniy Brikmanns Buch mit Azure App Services umsetzen würde.

Fangen wir zuerst mit dem Virtual Machine Scale Set an:

Wir brauchen eine Resource Group

### Resource Group

resource "azurerm_resource_group" "rg" {
  name     = "rg-vmssssample-test"
  location = "East US"
  tags = {
      App = "VMSS"
      Source = "Terraform"
  }
}

In diesem Beispiel beginnen wir mit Tags auf Resource-Group-Ebene für die bereitgestellte App und die Quelle. Außerdem möchte ich eine Namenskonvention basierend auf den Microsoft Best Practices aus diesem Artikel einführen.

Für eine Resource Group gibt es das vorgeschlagene Muster:
rg-<App- oder Dienstname>-<Abonnementtyp>-<### >

Als Nächstes - das vNet

### Netzwerk
resource "azurerm_virtual_network" "vNet" {
  name                = "vnet-shared-eastus-001"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  address_space       = ["10.0.0.0/16"]
  tags = azurerm_resource_group.rg.tags
}

Im vNet müssen wir das interne Subnetz für die VMs im Scale Set definieren:

### Subnetz

resource "azurerm_subnet" "sNet" {
  name                 = "snet-shared-vmsssample-001"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vNet.name
  address_prefix       = "10.0.2.0/24"
}

In meinem Skript füge ich die folgende Ressource vor der VM-Scale-Set-Definition hinzu, weil ich dem Load Balancer einen FQDN zuweisen möchte. Es gibt eine hilfreiche Terraform-Ressource zum Erstellen eines zufälligen Strings für den FQDN:

### Zufälliger FQDN-String
resource "random_string" "fqdn" {
 length  = 6
 special = false
 upper   = false
 number  = false
}

Einen Load Balancer in unser Skript integrieren

Das gängige Designmuster ist die Bereitstellung eines Load Balancers vor den VMs im Scale Set. Damit wird der eingehende Traffic zwischen den virtuellen Maschinen verteilt:

### Load-Balancer-Definition
resource "azurerm_lb" "vmsssample" {
 name                = "lb-vmsssample-test-001"
 location            = azurerm_resource_group.rg.location
 resource_group_name = azurerm_resource_group.rg.name

 frontend_ip_configuration {
   name                 = "ipconf-PublicIPAddress-test"
   public_ip_address_id = azurerm_public_ip.vmss-pip.id
 }

  tags = azurerm_resource_group.rg.tags
}

Der Load Balancer braucht weitere Konfiguration. Wir müssen einen Backend-IP-Pool sowie eine Probe zur Überprüfung des Gesundheitsstatus der VMs definieren:

### Backend-Pool definieren
resource "azurerm_lb_backend_address_pool" "vmsssample" {
 resource_group_name = azurerm_resource_group.rg.name
 loadbalancer_id     = azurerm_lb.vmsssample.id
 name                = "ipconf-BackEndAddressPool-test"
}

### LB-Probes definieren
resource "azurerm_lb_probe" "vmsssample" {
 resource_group_name = azurerm_resource_group.rg.name
 loadbalancer_id     = azurerm_lb.vmsssample.id
 name                = "http-running-probe"
 port                = 80
}

Der letzte Schritt ist die Regel für das Load Balancing – welcher Port soll ausbalanciert werden:

### LB-Regel definieren
resource "azurerm_lb_rule" "vmsssample" {
   resource_group_name            = azurerm_resource_group.rg.name
   loadbalancer_id                = azurerm_lb.vmsssample.id
   name                           = "http"
   protocol                       = "Tcp"
   frontend_port                  = 80
   backend_port                   = 80
   backend_address_pool_id        = azurerm_lb_backend_address_pool.vmsssample.id
   frontend_ip_configuration_name = "ipconf-PublicIPAddress-test"
   probe_id                       = azurerm_lb_probe.vmsssample.id
}

Wie in unserem Beispiel für eine einzelne VM ist es wichtig, die Netzwerksicherheitsgruppe zu definieren. Wir brauchen aber nicht den SSH-Port, nur Port 80 für den Webserver:

### NSG definieren
resource "azurerm_network_security_group" "vmsssample" {
    name                = "nsg-weballow-001"
    location            = azurerm_resource_group.rg.location
    resource_group_name = azurerm_resource_group.rg.name
    
     security_rule {
        name                       = "WebServer"
        priority                   = 1002
        direction                  = "Inbound"
        access                     = "Allow"
        protocol                   = "Tcp"
        source_port_range          = "*"
        destination_port_range     = "80"
        source_address_prefix      = "*"
        destination_address_prefix = "*"
     }
}

Das VM Scale Set selbst

### Das VM Scale Set (VMSS)
resource "azurerm_linux_virtual_machine_scale_set" "vmsssample" {
  name                = "vmss-vmsssample-test-001"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  sku                 = "Standard_B2s"
  instances           = 1
  admin_username      = "adminuser"
  admin_password      = "Password1234!"
  disable_password_authentication = false
  tags = azurerm_resource_group.rg.tags

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "16.04-LTS"
    version   = "latest"
  }

  os_disk {
    storage_account_type = "Standard_LRS"
    caching              = "ReadWrite"
  }

  network_interface {
      name    = "nic-01-vmsssample-test-001"
      primary = true

    ip_configuration {
        name      = "ipconf-vmssample-test"
        primary   = true
        subnet_id = azurerm_subnet.sNet.id
        load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.vmsssample.id]
    }
    network_security_group_id = azurerm_network_security_group.vmsssample.id
  }
}

Jetzt können wir unser Skript mit plan prüfen und mit apply auf unser Azure-Konto anwenden. Nun brauchen wir den Webserver in der Maschine. Dafür müssen wir eine neue Ressource bereitstellen - die “azurerm_virtual_machine_scale_set_extension”:

### Webserver zum VMSS hinzufügen
resource "azurerm_virtual_machine_scale_set_extension" "vmsssampleextension" {
  name                         = "ext-vmsssample-test"
  virtual_machine_scale_set_id = azurerm_linux_virtual_machine_scale_set.vmsssample.id
  publisher                    = "Microsoft.Azure.Extensions"
  type                         = "CustomScript"
  type_handler_version         = "2.0"
  auto_upgrade_minor_version   = true
  force_update_tag             = true
  
  settings = jsonencode({
      "commandToExecute" : "apt-get -y update && apt-get install -y apache2" 
    })
}

Bei meiner Recherche fand ich heraus, dass mit Terraform Version 0.12 die Funktion jsonencode implementiert wurde. Damit lässt sich ein String einfacher in JSON konvertieren.

Aber

Wenn wir jetzt unser Skript bereitstellen, haben wir alle Komponenten für ein Virtual Machine Scale Set mit installiertem Webserver. Wie anfangs erwähnt, funktioniert die Custom Script Extension nicht wie erwartet. Wenn man im Portal die Anzahl der Instanzen in den Skalierungsoptionen ändert, werden die Custom Script Extensions auf die VMs verteilt. Dann zeigt der Browser auf der öffentlichen IP die Apache-Standard-Website an.

VMSS Skalierungsoption

Nach dem Hochskalieren zeigt unser Skript den gewünschten Zustand beim Aufrufen des FQDN.

Der nächste Beitrag zeigt dann die Bereitstellung mit Azure App Services als Lösung für die gleiche Herausforderung.