Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Erstellen eines Docker Swarms auf Azure mit Terraform, Teil 2

15. Februar 2021

Portrait Bild von Tobias Fenster

Tobias Fenster

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

Es ist schon eine Weile her, aber in Teil 1 dieser Post-Serie habe ich die Gesamtarchitektur und den Ansatz zur Erstellung eines Windows-Docker Swarms auf Azure mit Terraform erklärt, vorbereitet mit Portainer und Traefik. In diesem zweiten Teil möchte ich tiefer in das Terraform-Setup sowie die dafür verwendeten Konfigurationsdateien einsteigen.

Das TL;DR

Ich gehe davon aus, dass Sie einige Grundkenntnisse über Terraform haben, aber wenn nicht, sollten Sie das sehr gute intro lesen, um eine Idee zu bekommen. Wenn Sie außerdem den Azure Provider für Terraform nicht kennen, sollten Sie einen Blick in die Dokumentation von Terraform oder die Dokumentation von Microsoft werfen. Damit sollten Sie in der Lage sein, die folgenden Dateien zu verstehen:

Damit wird die Infrastruktur erstellt. Dann wird mit Hilfe einiger Skripte, die ich im dritten Teil der Beitragsserie vorstellen werde, der OpenSSH-Server eingerichtet und auf der jumpbox mit einer passwortlosen Konfigurationsdatei konfiguriert, da man sich nicht mit einem Passwort mit einem extern erreichbaren Rechner verbinden können sollte. Die Manager und Worker haben eine SSH-Konfigurationsdatei, die den Zugriff mit Passwort erlaubt, da diese Rechner nur über die Jumbpox erreichbar sind.

Der letzte Abschnitt beschreibt die Docker ComposeKonfigurationsdatei, die sich um das Deployment von Traefik, Portainer und dem Portainer-Agent kümmert.

Die Details zum Deployment der Infrastruktur mit Terraform: Variablen und allgemeine Dinge

Ich kann nicht jede einzelne Zeile aller Terraform-Dateien durchgehen, weil das ein extrem langer Blogpost wäre und vieles davon ziemlich trivial ist, aber ich möchte auf ein paar spezielle Dinge hinweisen:

In der Datei variables.tf wird ein Local namens name eingerichtet, das als Präfix für fast alle Ressourcen verwendet wird. Es wird mit einem weiteren Präfix generiert, das standardmäßig auf swarm steht, aber durch etwas anderes überschrieben werden kann, wenn Sie möchten, dass Ihre Ressourcen anders beginnen. Es hat auch einen zufälligen Teil, was sinnvoll ist, wenn Sie die Infrastruktur vollständig oder teilweise automatisiert und oft aufsetzen, so dass Sie sich keine Sorgen um Namenskonflikte machen müssen. Aber wenn Sie einen sprechenderen Namen wollen, können Sie diesen Teil leicht ändern. So sieht der Aufbau in der Variablendatei aus:

variable "prefix" {
  description = "Prefix for all names"
  default     = "swarm"
}

locals {
  name = "${var.prefix}-${random_string.name.result}"
}

Die Zufallszeichenkette wird in der Datei common.tf wie folgt definiert, einschließlich des Providers random, den wir referenzieren müssen, um diese Funktionalität zu nutzen. Sie können auch sehen, dass wir denselben Provider verwenden, um ein zufälliges Passwort und die Ausgabe zu generieren, so dass wir am Ende das Passwort erhalten können:

provider "random" {
  version = "=2.3.0"
}

resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "_%@"
  min_lower        = 1
  min_numeric      = 1
  min_special      = 1
  min_upper        = 1
}

output "password" {
  value = random_password.password.result
}

Wenn Sie vergessen haben, das Passwort zu kopieren und irgendwo sicher zu speichern, können Sie jederzeit terraform output ausführen, um die Ausgabe wieder zu erhalten. Aber um zurück zum name zu kommen: Wenn Sie nur das Präfix ändern wollen, können Sie Ihre eigene .tfvars-Datei hinzufügen, um das zu überschreiben. Wenn Sie z.B. das Präfix cluster statt swarm verwenden wollen, würden Sie dies z.B. in eine Datei vars.tfvars schreiben:

prefix = "cluster"

Wenn Sie dann ein terraform apply -var-file vars.tfvars ausführen, wird die Variablendefinition aus der .tfvars-Datei übernommen und Ihr Präfix wird geändert. Sie finden auch andere Konfigurationsoptionen wie die zu verwendende Azure-Region (Variable location) oder komplexere Variablen wie die Größen- und SKU-Einstellungen für die VMs (z. B. die Variable workerVmssSettings). Sie sollten eine Beschreibung haben, um zu erklären, was sie tun, und Sie können die Einstellungen auch überschreiben. Dies ist z. B. die Standardeinstellung für die Worker-Konfiguration

variable "workerVmssSettings" {
  description = "The Azure VM scale set settings for the workers"
  default = {
    size       = "Standard_D8s_v3"
    number     = 2
    sku        = "2019-datacenter-core-with-containers"
    version    = "17763.1158.2004131759"
    diskSizeGb = 1024
  }
}

Nehmen wir an, Sie sind mit dem meisten davon einverstanden, möchten aber 4 Worker haben und einen 2004er Windows Server verwenden. In diesem Fall könnten Sie in Ihrer .tfvars-Datei etwas wie folgt aufnehmen

workerVmssSettings = {
    number     = 4
    sku        = "datacenter-core-2004-with-containers-smalldisk"
    version    = "latest"
}

Damit hätten Sie vier statt zwei Worker und würden das 2004er-Image anstelle des 2019er (1809er) verwenden.

Sie werden auch viele Variablen finden, die auf zusätzliche Skripte verweisen, aber das wird im nächsten Teil der Blogpost-Serie erklärt. In der Datei common.tf finden Sie auch die Azure-Ressourcengruppe, das virtuelle Netzwerk und den Azure Key Vault, aber diese haben keine spezielle Konfiguration. Das Einzige hier erwähnenswerte ist, dass ein öffentlicher Schlüssel am Standardspeicherort (~/.ssh/id_rsa.pub) gesucht und in den Key Vault hochgeladen wird; wenn Sie also Ihren öffentlichen Schlüssel an einem Nicht-Standardspeicherort speichern, müssen Sie diesen Teil ändern

resource "azurerm_key_vault_secret" "sshPubKey" {
  name         = "sshPubKey"
  value        = file(pathexpand("~/.ssh/id_rsa.pub"))
  key_vault_id = azurerm_key_vault.main.id
}

Die Details zum Bereitstellen der Infrastruktur mit Terraform: Gemeinsam genutzte Komponenten

Einige Komponenten der Infrastruktur werden von den anderen gemeinsam genutzt: Storage in storage.tf, der Azure Loadbalancer in loadbalancer.tf und die Jumpbox in jumpbox.tf. Auch das ist alles ziemlich einfach, es sind nur ein paar Dinge zu erwähnen:

Die Jumpbox hat zwei Sicherheitsregeln, eine, um SSH-Netzwerkverkehr zuzulassen, und eine, um RDP-Netzwerkverkehr explizit zu verweigern. Die zweite ist für den Fall vorgesehen, dass SSH aus irgendeinem Grund fehlschlägt und Sie ein Fallback haben möchten. Dann können Sie einfach zur Netzwerkkonfiguration im Azure Portal gehen und diese Regel von deny auf allow ändern oder sie in Terraform ändern und erneut anwenden.

resource "azurerm_network_security_rule" "ssh" {
  name                        = "sshIn"
  network_security_group_name = azurerm_network_security_group.jumpbox.name
  resource_group_name         = azurerm_resource_group.main.name
  priority                    = 300
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "22"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
}

resource "azurerm_network_security_rule" "rdp" {
  name                        = "rdpIn"
  network_security_group_name = azurerm_network_security_group.jumpbox.name
  resource_group_name         = azurerm_resource_group.main.name
  priority                    = 310
  direction                   = "Inbound"
  access                      = "Deny"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = "3389"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
}

Die Jumpbox muss auch den öffentlichen SSH-Schlüssel aus dem Azure Key Vault herunterladen und benötigt dafür die Berechtigung Get. Dies geschieht mit einer azurerm_key_vault_access_policy, die auf den Key Vault und den Principal der VM verweist. Das bedeutet, dass wir später in einem der Skripte Zugriff auf den Key Vault erhalten können.

resource "azurerm_key_vault_access_policy" "jumpbox" {
  key_vault_id = azurerm_key_vault.main.id
  object_id    = azurerm_windows_virtual_machine.jumpbox.identity.0.principal_id

  secret_permissions = [
    "Get"
  ]
  ...
}

Für den Load Balancer möchte ich noch einen Mechanismus erwähnen, der auch an anderen Stellen verwendet wird, um eine Ressource "bedingt" zu machen. Dies ist in Terraform derzeit nicht ohne weiteres mit so etwas wie einem enabled Flag möglich. Der Workaround ist die Verwendung der Eigenschaft count, die für das Deployment mehrerer Ressourcen mit der gleichen Konfiguration gedacht ist. In diesem Fall setze ich die Eigenschaft count entweder auf 1 oder auf 0, was sie im Wesentlichen zu einer bedingten Komponente macht. Ich habe eine Variable namens managerVmSettings.useThree, die konfiguriert, ob Sie einen oder drei Manager haben (drei sind vorzuziehen, da sie Ihnen Fehlertoleranz bieten, aber für ein einfaches Entwicklungs- oder Testszenario könnte einer ausreichen). Wenn diese Variable true ist, ist der count1. Wenn sie false ist, ist der count0. Hierfür können wir bequem ein ternäres if verwenden, z.B. für die Zuordnung des zweiten Managers zum Backend-Adresspool des Load Balancers:

resource "azurerm_network_interface_backend_address_pool_association" "mgr2" {
  count                   = var.managerVmSettings.useThree ? 1 : 0
  network_interface_id    = azurerm_network_interface.mgr2.0.id
  ip_configuration_name   = "static-mgr2"
  backend_address_pool_id = azurerm_lb_backend_address_pool.main.id
}

Der gleiche Mechanismus wird auch für den Manager und seine Netzwerkschnittstelle verwendet, daher können wir ihn nicht als azurerm_network_interface.mgr2.id referenzieren, sondern müssen stattdessen azurerm_network_interface.mgr2.0.id verwenden, da es sich um die erste Instanz handelt.

Die Details zum Deployment der Infrastruktur mit Terraform: Der Docker Swarm - Manager und Worker

Die Manager sind drei (oder eine) separat konfigurierte VMs, die in managers.tf definiert sind. Aus Terraform-Sicht wäre es einfacher gewesen, sie nur einmal zu konfigurieren und den count-Mechanismus zu verwenden, um einen oder drei zu haben, aber aufgrund der Art und Weise, wie das Einrichten eines Swarms funktioniert, brauchte ich die folgenden Dinge:

  • Der erste Manager muss etwas anderes tun als die anderen. Das wird als Skript-Parameter realisiert, was im nächsten Beitrag noch einmal genauer erklärt wird, aber die einzige gute Möglichkeit, das einzurichten, war eine andere Konfiguration für den ersten Manager.
  • Außerdem benötigt der erste Manager eine dedizierte, statische IP-Adresse, die er beim Erstellen und beim Beitritt zum Swarm bekannt gibt.
  • Der zweite Manager kann seine Konfiguration erst starten, wenn der erste komplett fertig ist. Das lässt sich leicht mit einer depends_on-Einstellung bewerkstelligen, die dem zweiten Manager sagt, dass er erst starten soll, wenn das Initialisierungsskript des ersten fertig ist, aber ich konnte keinen guten Weg finden, das zu implementieren, wenn man ein count-basiertes Setup verwendet.
resource "azurerm_windows_virtual_machine" "mgr2" {
  ...
  depends_on = [
    azurerm_virtual_machine_extension.initMgr1
  ]
}
  • Wenn der zweite und der dritte Manager gleichzeitig den Swarm Join Prozess starten, bekam ich manchmal die Fehlermeldung, dass nicht die Mehrheit der Manager vorhanden sei. Es sah für mich so aus, als wäre der zweite nicht vollständig da, aber bereits registriert und dann konnte der dritte nicht beitreten. Es war nicht ganz zuverlässig erfolgreich oder scheiterte, also scheint es eine Art Race Condition zu sein. Die Lösung war wieder eine depends_on Eigenschaft.
  • Der erste Manager muss die Swarm Join Token für Manager und Worker in den Azure Key Vault schreiben, also braucht er die Set Berechtigung und im Falle eines Re-Create auch Delete, während die anderen Manager (und Worker) nur Get benötigen.

Abgesehen davon haben die Manager keine spezielle Einrichtung in Terraform. Die Worker sind in workers.tf definiert, die mit einem Virtual Machine Scale Set implementiert sind, wie in Teil 1 dieser Blogpost-Serie erklärt, so dass die Terraform-Datei nur das beinhaltet und die Anzahl der Worker einfach über die SKU-Kapazität konfiguriert werden kann:

resource "azurerm_virtual_machine_scale_set" "worker" {
  ...
  sku {
    ...
    capacity = var.workerVmssSettings.number
  }
  ...
}

Der Rest ist wieder nichts Besonderes.

Die Details: OpenSSH konfigurieren

Die OpenSSH-Konfiguration hat zwei verschiedene Varianten: Die Jumpbox ist der einzige Rechner, auf dem SSH öffentlich verfügbar ist, daher ist in der Konfiguration nur die Public-Key-Authentifizierung aktiviert und die Passwort-Authentifizierung deaktiviert. Ich hatte einige Probleme mit fehlgeschlagenen Verbindungen nach ein paar Minuten, also habe ich auch ClientAliveInterval von 60 (Sekunden) hinzugefügt, wie hier erklärt, der Rest ist wieder ziemlich Standard:

Port 22
PubkeyAuthentication yes
PasswordAuthentication no
ClientAliveInterval 60
Subsystem	sftp	sftp-server.exe
Match Group administrators
       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

Bei den Managern und Workern ist die Passwortauthentifizierung aktiviert, so dass Sie sich von der Jumbpox aus mit einem Passwort anmelden können. Daher ist die einzige andere Zeile in der Konfiguration für diese Einrichtung diese:

PasswordAuthentication yes

Der vierte Teil dieser Blogpost-Serie wird Ihnen zeigen, wie Sie stattdessen ein privates/öffentliches SSH-Schlüssel-Setup auf der Jumpbox verwenden können.

Die Details: Verwendung einer Docker-Compose-Datei zum Deployment von Traefik und Portainer als Swarm Service

Damit ist die Infrastruktur vorhanden, und wir haben einen Docker Swarm mit einem oder drei Managern und einer konfigurierbaren Anzahl von Workern. Jetzt ist es an der Zeit, Swarm Services zu deployen, im Grunde Docker-Container mit einer konfigurierbaren Anzahl von Replikaten und Platzierungsregeln (z. B. "überall" oder "nur auf Managern"). Dazu habe ich eine Docker-Compose-Datei verwendet, um einen Stack, also eine Sammlung von Diensten, zu definieren. Die Konfigurationsdatei ist eine Vorlagendatei mit ein paar Platzhaltern wie $email und $externaldns, die auch durch die Skripte ersetzt werden, so dass etwas wie

--certificatesresolvers.myresolver.acme.email=$email

zu so etwas wird

--certificatesresolvers.myresolver.acme.email=tobias.fenster@cosmoconsult.com

Die Traefik-Konfiguration hat diese bemerkenswerten Teile:

  • Sie verwendet ein spezielles Image für Traefik, weil ich die Möglichkeit haben wollte, ein Multi-Arch-Image zu haben, wie hier erklärt, und Docker-Bibliotheks-Images wie traefik können das nicht tun. Seltsam, aber ich habe hier gefragt und das als Antwort bekommen. Daher referenziere ich nicht das Standard-Image:
services:
  traefik:
    image: tobiasfenster/french-reverse-proxy:2.3.4-windowsservercore
...
  • Die Konfiguration, um die Traefik-API und das Dashboard über Traefik selbst verfügbar zu machen, ist bereits vorhanden, aber die Nutzung ist deaktiviert. Für den Fall, dass Sie es z.B. zum Debuggen benötigen, müssen Sie nur den boolschen Wert in Zeile 3 auf true setzen:
...
      labels:
        - traefik.enable=false
        - traefik.http.routers.api.entrypoints=websecure
        - traefik.http.routers.api.tls.certresolver=myresolver
        - traefik.http.routers.api.rule=Host(``$externaldns``) && (PathPrefix(``/api``) || PathPrefix(``/dashboard``))
        - traefik.http.routers.api.service=api@internal
        - traefik.http.services.api.loadBalancer.server.port=8080
...
  • Ich habe ein gemeinsames Azure File Share als Laufwerk s: in allen Knoten des Swarms, so dass ich das Let's Encrypt Zertifikate für Traefik dort ablegen kann und mich nicht darum kümmern muss, auf welchem Manager der Traefik-Container auftaucht. Und natürlich muss Traefik im Docker Swarm Modus laufen:
services:
  traefik:
    command:
      ...
      - --providers.docker.swarmMode=true
      ...
      - --certificatesresolvers.myresolver.acme.storage=c:/le/acme.json
      ...
    volumes:
      - source: 'S:/le'
        target: 'C:/le'
        type: bind
...

Der Haupt-Service von Portainer speichert seine Daten ebenfalls auf dieser Freigabe, was wiederum bedeutet, dass es mir egal ist, auf welchem Manager der Container auftaucht. Ich gebe auch das generierte Passwort als [Swarm Geheimnis][swarm-secret] frei und stelle das Portainer zur Verfügung, so dass das Admin-Passwort dort das gleiche ist wie bei den VMs. Nach einiger Nutzung stieß ich dann auf das Problem, dass ich größere Dateien nicht über den Portainer-Dateibrowser übertragen konnte. Ich fand heraus, dass dies durch Traefik verursacht wurde und erhöhte auch dort das Größenlimit (maxRequestBodyBytes):

Dienste:
...
  portainer:
    image: portainer/portainer-ce:latest
    command: -H tcp://tasks.agent:9001 --tlsskipverify --admin-password-file 'c:\\secrets\\adminPwd'
    volumes:
      - s:/portainer-data:c:/data
    ...
    deploy:
      ...
      labels:
        ...
        - traefik.http.middlewares.limit.buffering.maxRequestBodyBytes=500000000
        - traefik.http.routers.portainer.middlewares=portainer@docker, limit@docker
    ...
    secrets:
      - source: adminPwd
        target: "c:/secrets/adminPwd"
...
secrets:
  adminPwd:
    external: true

Der Portainer-Agent hat nur eine nennenswerte Einstellung: Der Pfad für die Docker-Volumes muss anders konfiguriert werden, wenn man ihn vom Standardpfad C:\ProgramData\docker\volumes wegbewegt. Ich lege zu diesem Zweck eine größere Datenplatte auf den Workern an, so dass diese zu f:\dockerdata\volumes wird. Da die Agenten auf allen Knoten laufen müssen, bedeutet dies auch, dass wir ein f:-Laufwerk auf den Managern benötigen, obwohl ich den Docker-Datenpfad nicht dorthin verschoben habe, da es sonst zu einem Fehler beim Deployment kommt. In der Konfiguration sieht das so aus:

services:
...
  agent:
    image: portainer/agent:latest
    ...
    volumes:
      ...
      - source: '$dockerdatapath/volumes'
        target: 'C:/ProgramData/docker/volumes'
        type: bind
...

Dies sollte hoffentlich alle relevanten, nicht standardmäßigen Aspekte meines Setups erklären. Der nächste Teil behandelt die PowerShell-Skripte, die zum Einrichten und Konfigurieren der VMs und von Docker Swarm verwendet werden.

Hinweis: Dieser Post ist ursprünglich auf Englisch auf dem Blog des Authors erschienen.