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

TechWiese Blog

Erstellen eines Docker Swarms auf Azure mit Terraform, Teil 3

19. März 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.

Nach der allgemeinen Einrichtung in Teil eins und der Einrichtung von Docker Swarm, Terraform, OpenSSH und Docker Compose für Portainer und Traefik in Teil zwei, möchte ich in diesem dritten Teil die PowerShell-Skripte durchgehen, die bei der Einrichtung des Docker Swarms auf Azure verwendet werden.

Die TL;DR

Der Großteil der Arbeit wird in vier Skripten erledigt, wobei das jeweils erste Skript als VM-Erweiterung ("VM Extension") aufgerufen wird:

Der Grund dafür, dass ich nicht alles in ein Skript für die Manager und ein Skript für die Worker packe, ist, dass die Initialisierung des Managers, insbesondere des ersten, vor allem vor allem anderen passieren muss, daher mache ich das sofort. Der Rest kann später passieren, also erstelle ich einfach eine Aufgabe ("Scheduled Task") dafür, damit das erste Skript fertig wird. Und ich wollte einfache Erweiterungsmöglichkeiten haben, um diese ganze Einrichtung entweder beim ersten Start oder beim Neustart eines Managers oder Workers erweitern zu können, also habe ich auch Aufgaben beim Neustart für diese Konfigurationsskripte hinzugefügt. Der Rest sind relativ einfache Skripte zum Konfigurieren der Jumpbox, Einrichten des PowerShell-Profils und Einhängen der Azure-Dateifreigabe.

Die Details: Integration in die Terraform-Templates

Die Einbindung in die Terraform-Templates ist konzeptionell immer gleich, als Erweiterung der Virtual Machine. Sie hat eine Datei-URL, unter der das Skript heruntergeladen werden kann und eine geschützte Einstellung, die den auszuführenden Befehl enthält. Dies ist z.B. die Terraform-Ressource, um das Skript mgrInitSwarmAndSetupTasks.ps1 für den ersten Manager aufzurufen:

resource "azurerm_virtual_machine_extension" "initMgr1" {
  name = "initMgr1"
  virtual_machine_id = azurerm_windows_virtual_machine.mgr1.id
  ...

  settings = jsonencode({
    "fileUris" = [
      "https://raw.githubusercontent.com/cosmoconsult/azure-swarm/${var.branch}/scripts/mgrInitSwarmAndSetupTasks.ps1"
    ]
  })

  protected_settings = jsonencode({
    "commandToExecute" = "powershell -ExecutionPolicy Unrestricted -File mgrInitSwarmAndSetupTasks.ps1 
    -externaldns \"${local.name}.${var.location}.cloudapp.azure.com\" -email \"${var.eMail}\" 
    -branch \"${var.branch}\" -additionalPreScript \"${var.additionalPreScriptMgr}\" 
    -additionalPostScript \"${var.additionalPostScriptMgr}\" -dockerdatapath \"${var.dockerdatapath}\" 
    -name \"${local.name}\" -storageAccountName \"${azurerm_storage_account.main.name}\" 
    -storageAccountKey \"${azurerm_storage_account.main.primary_access_key}\" 
    -adminPwd \"${random_password.password.result}\" -isFirstmgr 
    -authToken \"${var.authHeaderValue}\" -debugScripts \"${var.debugScripts}\"
  })
}

Sie können sehen, dass ziemlich viele Parameter verwendet werden, um alles nach Bedarf zu konfigurieren, und Sie können sehen, dass fast alle aus Terraform-Variablen wie ${var.eMail} oder Eigenschaften von Terraform-Ressourcen wie ${azurerm_storage_account.main.name} gelesen werden.

Die Details: Initialisieren und Konfigurieren der Manager

Die Initialisierung und Konfiguration der Manager erfolgt in zwei Teilen: Zuerst wird der Swarm selbst in mgrInitSwarmAndSetupTasks.ps1 eingerichtet und dann wird eine Aufgabe für alles andere in mgrConfig.ps1 gestartet, wie im TL;DR erklärt. Auf allen Managern (und Workern, wie wir später sehen werden) muss die Firewall drei Ports offen haben, das geschieht daher vor der Initialisierung des Swarms selbst:

New-NetFirewallRule -DisplayName "Allow Swarm TCP" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 2377, 7946 | Out-Null
New-NetFirewallRule -DisplayName "Allow Swarm UDP" -Direction Inbound -Action Allow -Protocol UDP -LocalPort 4789, 7946 | Out-Null

Wenn dies der erste Manager ist, erfolgt die Initialisierung mit einer festen IP-Adresse, wie in Teil zwei erklärt. Beachten Sie auch, dass der Standard-Adresspool für Container im Swarm 10.10.0.0/16 ist, um sicherzustellen, dass dies nicht mit der Azure-Infrastruktur kollidiert.

Invoke-Ausdruck "docker swarm init --advertise-addr 10.0.3.4 --default-addr-pool 10.10.0.0/16"

Danach wird das Admin-Passwort als Swarm-Geheimnis gespeichert, so dass wir es später beim Starten von Portainer abrufen können, was auch in Teil zwei erklärt wird.

Out-File -FilePath ".\adminPwd" -NoNewline -InputObject $adminPwd -Encoding ascii
docker secret create adminPwd ".\adminPwd"
Remove-Item ".\adminPwd"

Nachdem der Swarm eingerichtet ist, können wir nun die Token abrufen, um dem Swarm als Worker und als Manager beizutreten:

$token = Invoke-Expression "docker swarm join-token -q worker"
$tokenMgr = Invoke-Ausdruck "docker swarm join-token -q manager"

Da diese Token von jedem anderen Manager oder Worker benötigt werden, werden sie im Azure Key Vault gespeichert. Wir haben den VMs Zugriff darauf gegeben (der erste Manager hat Schreibzugriff, alle anderen nur Lesezugriff), aber wir müssen ein Zugriffstoken erhalten, um uns zu authentifizieren. Dazu rufen wir eine spezielle URL auf, die das Zugriffstoken zurückgibt, und verwenden dieses, um einen HTTP PUT-Aufruf an die Key Vault API zu machen

$content = [DownloadWithRetry]::DoDownloadWithRetry('http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net', 5, 10, $null, $null, $true) | ConvertFrom-Json
$KeyVaultToken = $content.access_token
$joinCommand = "docker swarm join --token $token 10.0.3.4:2377"
$Body = @{
    value = $joinCommand
}
$result = Invoke-WebRequest -Uri https://$name-vault.vault.azure.net/secrets/JoinCommand?api-version=2016-10-01 -Method PUT -Headers @{Authorization = "Bearer $KeyVaultToken" } -Body (ConvertTo-Json $Body) -ContentType "application/json" -UseBasicParsing

$joinCommandMgr = "docker swarm join --token $tokenMgr 10.0.3.4:2377"
$Body = @{
    value = $joinCommandMgr
}
$result = Invoke-WebRequest -Uri https://$name-vault.vault.azure.net/secrets/JoinCommandMgr?api-version=2016-10-01 -Method PUT -Headers @{Authorization = "Bearer $KeyVaultToken" } -Body (ConvertTo-Json $Body) -ContentType "application/json" -UseBasicParsing

Da das manchmal nicht zuverlässig funktioniert hat, versuche ich auch, die Secrets zu lesen, um sicherzustellen, dass sie vorhanden sind, und habe es sogar in einen try / catch-Block verpackt und führe 10 Wiederholungen durch, wenn etwas fehlschlägt. Eventuell etwas übervorsichtig, aber das führt dazu, dass die Deployments mittlerweile sehr zuverlässig und stabil funktionieren

$secretJson = (Invoke-WebRequest -Uri https://$name-vault.vault.azure.net/secrets/JoinCommand?api-version=2016-10-01 -Method GET -Headers @{Authorization = "Bearer $KeyVaultToken" } -UseBasicParsing).content | ConvertFrom-Json
Write-Debug "worker join command result: $secretJson"
$secretJsonMgr = (Invoke-WebRequest -Uri https://$name-vault.vault.azure.net/secrets/JoinCommandMgr?api-version=2016-10-01 -Method GET -Headers @{Authorization = "Bearer $KeyVaultToken" } -UseBasicParsing).content | ConvertFrom-Json
Write-Debug "manager join command result: $secretJsonMgr"

if ($secretJson.value -eq $joinCommand -und $secretJsonMgr.value -eq $joinCommandMgr) {
    Write-Debug "join commands are matching"
    $tries = 11
}

Wenn einer der anderen Manager dieses Skript ausführt, muss er ebenfalls das Auth-Token holen, macht dann aber einen HTTP-GET-Aufruf, um das Swarm-Join-Token zu holen und führt dann den Join aus. Ich hatte auf früheren Windows-Versionen ziemliche Probleme, das zuverlässig zum Laufen zu bringen, wenn ich versuchte, 1-Core-Manager zu verwenden und konnte es nur mit 2-Core-Maschinen gut zum Laufen bringen. Kürzlich habe ich einige Tests mit Windows Server 2004 gemacht und das scheint viel besser zu funktionieren, aber wegen dieser Probleme ist dieser Teil viel komplizierter und beinhaltet mehr Jobs und die Überprüfung ihrer Zustände als nötig. Daher zeige ich hier eine vereinfachte Version, aber Sie können sich den tatsächlichen Code hier ansehen, wenn Sie möchten

$content = [DownloadWithRetry]::DoDownloadWithRetry('http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fvault.azure.net', 5, 10, $null, $null, $true) | ConvertFrom-Json
$KeyVaultToken = $content.access_token
$secretJson = [DownloadWithRetry]::DoDownloadWithRetry("https://$name-vault.vault.azure.net/secrets/JoinCommandMgr?api-version=2016-10-01", 30, 10, "Bearer $KeyVaultToken", $null, $false) | ConvertFrom-Json
Invoke-Ausdruck "$(secretJson.value)"

Der Rest lädt die anderen Skripte herunter und ruft sie auf oder erstellt die Aufgabe, um sie beim Neustart aufzurufen.

Das Skript mgrConfig.ps1 erledigt etwas Vorbereitungsarbeit und erstellt, wenn es auf dem ersten Manager aufgerufen wird, ein Overlay-Netzwerk für alle Swarm-Dienste, die über traefik verfügbar sein sollen. Dann lädt es die in Teil 1 erläuterte Docker Compose-Vorlagendatei herunter, ersetzt die Variablen in der Datei durch aktuelle Werte (Zeile 7 und 8) und stellt sie bereit.

if ($isFirstMgr) {
    Invoke-Ausdruck "docker network create --driver=overlay traefik-public" | Out-Null
    Start-Sleep -Seconds 10

    [DownloadWithRetry]::DoDownloadWithRetry("https://raw.githubusercontent.com/cosmoconsult/azure-swarm/$branch/configs/docker-compose.yml.template", 5, 10, $null, 's:\compose\base\docker-compose.yml.template', $false)
    $template = Get-Content 's:\compose\base\docker-compose.yml.template' -Raw
    $expanded = Invoke-Expression "@`"`r`n$template`r`n`"@"
    $expanded | Out-File "s:\compose\base\docker-compose.yml" -Encoding ASCII

    Invoke-Ausdruck "docker stack deploy -c s:\compose\base\docker-compose.yml base"
}

Auf allen Managern wird SSH eingerichtet, indem es (zusammen mit vim) über chocolatey installiert wird, der öffentliche SSH-Schlüssel aus dem Key Vault heruntergeladen wird, der über das Terraform-Deployment hochgeladen wurde, und SSH konfiguriert wird. Auch hier zeige ich eine leicht vereinfachte und weniger fehlertolerante Version, um Ihnen die Idee zu zeigen, und Sie können die echte Implementierung hier erhalten

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco feature enable -n allowGlobalConfirmation
choco install --no-progress --limit-output vim
choco install --no-progress --limit-output openssh -params '"/SSHServerFeature"'

[DownloadWithRetry]::DoDownloadWithRetry("https://raw.githubusercontent.com/cosmoconsult/azure-swarm/$branch/configs/sshd_config_wpwd", 5, 10, $null, 'C:\ProgramData\ssh\sshd_config', $false)

$secretJson = [DownloadWithRetry]::DoDownloadWithRetry("https://$name-vault.vault.azure.net/secrets/sshPubKey?api-version=2016-10-01", 5, 10, "Bearer $KeyVaultToken", $null, $false) | ConvertFrom-Json
$secretJson.value | Out-File 'c:\ProgramData\ssh\administrators_authorized_keys' -Encoding utf8

### adapted (pretty much copied) from https://gitlab.com/DarwinJS/ChocoPackages/-/blob/master/openssh/tools/chocolateyinstall.ps1#L433
$path = "c:\ProgramData\ssh\administrators_authorized_keys"
$acl = Get-Acl -Path $path
$acl.SetSecurityDescriptorSddlForm("O:BAD:PAI(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)")
Set-Acl -Path $path -AclObject $acl
### end of copy

Die Details: Initialisieren und Konfigurieren der Worker

Die Initialisierung und Konfiguration der Worker hat ebenfalls einen ersten Teil in workerSetupTasks.ps1, der mehr oder weniger nur den geplanten Task zum Aufruf von workerConfig.ps1 einrichtet. Das Config-Skript führt das gleiche Swarm-Netzwerk-Setup und die SSH-Installation mit chocolatey durch wie bei den Managern. Der Beitritt zum Swarm sieht auch sehr ähnlich aus wie auf dem Manager, natürlich unter Verwendung des Worker-Join-Tokens, nicht des Manager-Join-Tokens. Es gibt auch eine Option zum Herunterladen von Images, wenn der Worker hochfährt, so dass, wenn Sie im Voraus wissen, dass Sie einen bestimmten Workload ausführen werden, die Images bereits heruntergeladen werden, wenn die Anfrage eingeht.

Write-Host "pull $images"
if (-not [string]::IsNullOrEmpty($images)) {
    $imgArray = $images.Split(',');
    foreach ($img in $imgArray) {
        Write-Host "pull $img"
        Invoke-Ausdruck "docker pull $img" | Out-Null
    }
}

Die Details: Konfigurieren der jumpbox, Einrichten der PowerShell-Profile und Mounten des Azure File Share

Die Initialisierung der Jumpbox in jumpboxConfig.ps1 ist noch einfacher, sie richtet nur SSH ein und führt die folgenden drei allgemeinen Dinge aus, die auch auf den Workern und Managern vorkommen:

  • Es richtet das PowerShell-Profil so ein, dass es den aktuellen Hostnamen anzeigt, da es sonst sehr verwirrend werden kann, wenn man zwischen den verschiedenen Maschinen hin und her springt. Dazu lädt es das folgende Skript profile.ps1 herunter und legt es an dem speziellen Ort $PROFILE.AllUsersAllHosts ab, d. h. es wird immer dann ausgeführt, wenn eine PowerShell-Sitzung auf diesem Computer gestartet wird:
function prompt { "PS [$env:COMPUTERNAME]:$($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) " }
  • Es macht PowerShell zum Standard, wenn eine OpenSSH-Verbindung eingeht (normaler Standard ist cmd) mit der folgenden Zeile
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
  • Mit dem folgenden Skript mountAzFileShare.ps1 wird die Azure-Dateifreigabe gemountet, die als gemeinsamer Speicher für alle Nodes dient. Es ist wichtig, die Zugriffsrechte mit icacls wie unten zu setzen, da sonst die Container möglicherweise nicht den notwendigen Zugriff haben, was zu seltsamen Fehlern führt, nachdem der Container für einige Zeit läuft.
$secpassword = ConvertTo-SecureString $storageAccountKey -AsPlainText -Force
$creds = New-Object System.Management.Automation.PSCredential("Azure\$storageAccountName", $secpassword)
New-SmbGlobalMapping -RemotePath "\\$storageAccountName.file.core.windows.net\share" -Credential $creds -LocalPath "$($driveLetter):" -Persistent $true -RequirePrivacy $true
Invoke-Expression "icacls.exe $($driveLetter):\ /grant 'Everyone:(OI)(CI)(F)'"

Die Details: Zusätzliche Skripte einrichten

Als Letztes ist zu erwähnen, dass es für alle verschiedenen Maschinen (Jumpbox, Worker, Manager) Hook-Points für zusätzliche Skripte gibt. Damit können Sie auf meiner Infrastruktur aufbauen und zusätzliche Funktionen ausführen, z. B. zusätzliche Dienste bereitstellen oder spezielle Konfigurationen vornehmen. Um dies zu erreichen, haben alle Skripte am Anfang etwas Ähnliches wie das Folgende:

if ($additionalPreScript -ne "") {
    [DownloadWithRetry]::DoDownloadWithRetry($additionalPreScript, 5, 10, $authToken, 'c:\scripts\additionalPreScript.ps1', $false)
    
    & 'c:\scripts\additionalPreScript.ps1' -branch "$branch" -isFirstMgr:$isFirstMgr -authToken "$authToken"
}

Und am Ende steht dann noch etwas ähnliches wie das Folgende:

if (-not $restart) {
    if ($additionalPostScript -ne "") {
        [DownloadWithRetry]::DoDownloadWithRetry($additionalPostScript, 5, 10, $authToken, 'c:\scripts\additionalPostScript.ps1', $false)
        & 'c:\scripts\additionalPostScript.ps1' -branch "$branch" -externaldns "$externaldns" -isFirstMgr:$isFirstMgr -authToken "$authToken"
    }
}
else {
    if ($additionalPostScript -ne "") {
        & 'c:\scripts\additionalPostScript.ps1' -branch "$branch" -externaldns "$externaldns" -isFirstMgr:$isFirstMgr -authToken "$authToken" -neu starten 
    }
}

Wie Sie sehen können, habe ich eine eigene Download-Funktion implementiert, die in jedes Skript eingebettet ist, anstatt eines der Standard-Cmdlets zu verwenden, hauptsächlich aus zwei Gründen:

  • Manchmal schlägt der externe Download fehl, wenn der Rechner gerade erst startet, also brauchte ich einen Wiederholungsversuch, und der ist in der PowerShell erst ab V6 verfügbar, während ich meist noch bei V5.1 stehe.
  • Ich wollte eine einfache Möglichkeit, ein Auth-Token hinzuzufügen. Wenn der Parameter gesetzt ist, wird versucht, mit dem Auth-Token herunterzuladen und dann nochmal ohne.

Damit hoffe ich, dass Sie einen guten Überblick darüber haben, was während der Einrichtung und Konfiguration des Swarms und aller Komponenten in den PowerShell-Skripten passiert.

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