Shell-Skripte auf AWS EC2 mit SSM und Terraform - So funktioniert's

Wie man mit dem ‚AWS systems manager‘ benutzerdefinierte Skripte auf AWS EC2-Instanzen automatisiert ausführt, zeigen wir in diesem Artikel.



In diesem Artikel zeige ich wie mit dem ‘AWS Systems Manager’ ein Skript auf einer AWS EC2-Instanz automatisiert ausgeführt werden kann.

Der Inhalt des Skripts selbst ist nicht wichtig - Interessant ist eher wie ich mit folgenden Anforderungen umgegangen bin:

  1. Das Skript wird als AWS Systems Manager (SSM) Dokument erstellt
  2. Das SSM-Dokument wird mittels Terraform (Infrastructure-as-Code, IaC) verwaltet
  3. Das Skript soll in einem GIT repository as Datei eingecheckt und verwaltet werden
  4. Auf der EC2-Instanz darf das Skript nicht als ‘root’ Benutzer mit privilegierten Rechten ausgeführt werden, aus Sicherheitsgründen

Nutzen einer Terraform Resource

Der Terraform-Provider von AWS bringt alles mit was wir brauchen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            "echo Hallo, ich bin $(whoami)"
          ]
        }
      }
    ]
  })
}

Diese Resourcendefinition erstellt ein SSM-Dokument, das einen Einzeiler auf der EC2-Instanz ausführt. Es wird „Hallo, ich bin “ auf der Konsole ausgegeben, gefolgt vom Namen des Benutzers, der das Skript ausführt. Das Ergebnis ist „Hallo, ich bin root“ – was uns zeigt, dass Skripte standardmäßig als Root-Benutzer ausgeführt werden (siehe Anforderung 3). Man kann hier auch mehrere Befehle Zeile für Zeile definieren.

Das SSM-Dokument kann mittels der Terraform-Resource „aws_ssm_association“ mit einer EC2-Instanz verknüpft werden.

Ein Skript als Datei referenzieren

Terraform bietet eine „file“ Funkton an den um Inhalt einer Datei weiter zu verwenden. Wenn wir folgende Datei „beispiel.sh“ erstellen …

1
2
#!/bin/bash
echo Hallo, ich bin $(whoami)

… können wir sie ganz leicht in Terraform einbinden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "Mein Beispiel",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            file("./beispiel.sh")
          ]
        }
      }
    ]
  })
}

Das Skript als Nicht-Root ausführen

Jetzt wird es knifflig. Wir haben bereits gesehen, dass unser Skript als Root-Benutzer ausgeführt wird. Wenn wir möchten, dass Befehle als Nicht-Root ausgeführt werden, können wir den Linux-Befehl „su“ (Benutzer wechseln) verwenden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
              "su -c 'echo Hallo, ich bin $(whoami)' ssm-user"
          ]
        }
      }
    ]
  })
}

Das Ergebnis ist „Hallo, ich bin ssm-user. Der ssm-user ist auf jeder EC2-Instanz vorkonfiguriert und daher direkt nutzbar.

Wenn wir eine Datei wie oben gezeigt übergeben möchten, wird dies nicht funktionieren, zumindest nicht für mehrzeilige Skripte. Die Skriptdatei wird Zeile für Zeile von Terraform eingefügt, und jede Änderung (z. B. das Hinzufügen von su -c am Anfang zerstört die Struktur der Skriptdatei).

Die Lösung besteht darin, die Heredoc-Funktionalität von Bash zu verwenden, um mehrzeilige Anweisungen zu übergeben. Wir speichern das gesamte Skript vorübergehend in einer Bash-Variablen namens multi_line_statement (die ersten drei Zeilen im runCommand-Block), speichern die Variable in einer Datei auf der EC2-Instanz (drei Zeilen danach) und führen sie schließlich als Nicht-Root aus (die letzten beiden Zeilen). Das sieht dann so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
resource "aws_ssm_document" "example" {
  name          = "example_document"
  document_type = "Command"
  content = jsonencode({
    schemaVersion = "2.2",
    description   = "My example script",
    mainSteps = [
      {
        action = "aws:runShellScript",
        name   = "runShellScript"
        inputs = {
          runCommand = [
            "multiline_statement='",
            file("./beispiel.sh"),
            "'",
            "cat << EOF > /tmp/beispiel.sh",
            "$multiline_statement",
            "EOF",
            "chmod 777 /tmp/beispiel.sh",
            "su -c /tmp/beispiel.sh ssm-user"
          ]
        }
      }
    ]
  })
}

Solange im Skript keine Anführungszeichen verwendet werden funktionert alles reibungslos.

Fehleranalyse

Falls es beim Ausführen des Skriptes Probleme gibt, möchte man wissen was auf der EC2-Instanz genau vor sich geht. Die Skripte von SSM werden unter folgendem Pfad auf der Instanz gespeichert: /var/lib/amazon/ssm/{INSTANCE_ID}/document/orchestration/{COMMAND_ID}/runShellScript/_script.sh

Dort kann man das Skript lokal ausführen. Allerdings ‘gehört’ es dem ‘Root’ Benutzer. Entsprechend muss man selbst als ‘Root’ eingeloggt sein oder das Kommando ‘sudo’ nutzen.

Über den Autor

img/team/lohausen.jpg Daniel Lohausen

Daniel Lohausen

Als Cloud-Architekt und Prozess-Coach vereint Daniel Lohausen zwei Kernelemente erfolgreicher Softwareentwicklung in einem ganzheitlichen Beratungsansatz. Er ist überzeugt, dass sich technische und prozedurale Innovationen positiv beeinflussen und man die besten Ergebnisse erzielt, wenn man bei beide Aspekte konstant weiter entwickelt.