Wenn man einen VPS betreibt, dann möchte man seine Anwendung mittels CI/CD automatisch auf diesem ausrollen. Abseits von Docker Images kann hierfür auch SSH in der GitHub Actions Pipeline verwendet werden. Ich zeige dir hier du das mit SSH und ohne Passwort löst.

Vorbereitung

💡 Das gute zuerst: diese Anleitung ist nicht auf GitHub Actions beschränkt, sondern lässt sich in jedem Build Server (wie zum Beispiel Jenkins oder GitLab) verwenden, da native Linux-Befehle verwendet werden.

Zuerst brauchen wir einen SSH-Key. Wir loggen uns auf unserem Server ein und führen folgenden Befehl aus:

ssh-keygen

Die Fragen können wir alle mit den Standard-Werten belassen und einfach mit Enter bestätigen (auch das Passwort sollte leer bleiben). Haben wir unseren Key erstellt, lassen wir uns den Inhalt des Private Keys (ja, richtig, der Private Schlüssel) anzeigen:

cat ~/.ssh/id_ras

Den Schlüssel kopieren wir uns und speichern ihn als Repository Secret in unserem GitHub Repository ab.

  1. In der Web Oberfläche von GitHub das Repository öffnen
  2. den Tab “Settings” auswählen
  3. Zu “Secrets and Variables” -> “Actions” im Menü navigieren
  4. “New Repository Secret” klicken

Der Inhalt ist der eben kopierte Key, als Namen des Secrets wählen wir SSH_KEY.

Zusätzlich legen wir uns gleich noch ein zweites Secret mit dem Namen SERVER_IP an und schreiben unserer Server-IP Adresse hinein.

Generierten Key autorisieren

Damit der generierte Public Key auch als autorisiert gilt, muss dieser im FIle .ssh/authorized-keys hinzugefügt werden. Folgender Befehl hilft dabei:

cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

Wurde der Name des generierten Files geändert, muss statt id_rsa.pub hier der Name des generierten Files aus Schritt 1 angegeben werden.

Deployment Pipeline

Jetzt können wir in unserem Repository auch schon die GitHub Actions Pipeline anlegen. Im root unseres Repositories erstellen wir die Ordner-Struktur .github/workflows.

Anschließend erstellen wir ein File namens ci_cd.yml und fügen folgenden Inhalt ein:

name: Build and Deploy
on:
  push:
    branches:
      - main
  
jobs:
  build-and-deploy:
	env:
      SSH_PRIVATE_KEY: ${{secrets.SSH_KEY}}
      SERVER_IP: ${{secrets.SERVER_IP}}
      
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@master
      - name: build
        run: |
          echo 'foo bar' > foobar.txt          
      - name: Deploy
        run: |
          which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y  )
          eval $(ssh-agent -s)
          ssh-add <(echo "$SSH_PRIVATE_KEY" )
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          ssh-keyscan $SERVER_IP >> ~/.ssh/known_hosts
          chmod 644 ~/.ssh/known_hosts
          scp -r foobar.txt root@$SERVER_IP:/var/www/html          

Was passiert in dieser Pipeline?

  1. Wir bereiten zuerst unsere Environment Variables mit den angelegten Secrets vor
  2. Unser Repository wird ausgecheckt und gebaut (hier symbolisch mit dem erstellen der Datei foobar.txt. In eurem Repository kommen hier natürlich die Build Steps hinein)
  3. Unsere VM wird für den Build-Prozess vorbereitet und der ssh-agent installiert (falls nicht vorhanden)
  4. unseren erstellen Key fügen wir nun mittels ssh-add hinzu
  5. die Server-IP wird als bekannter Host eingetragen um Rückfragen für unbekannte Hosts zu vermeiden
  6. wir kopieren unseren Inhalt auf unseren Server (hier muss foobar.txt natürlich mit dem Build Artifact eurer Anwendung ersetzt werden)

Der Trick

Wir sehen hier, das wir uns eines kleinen “Tricks” bedient haben. Verbinden wir uns auf dem regulären Wege per SSH auf einen Server mittels SSH-Keys, so generieren wir einen Schlüssel auf unserem Rechner und übertragen nur den Public Key.

Da wir pro Build aber eine neue VM aufsetzen, in der unsere Pipeline-Schritte ausgeführt werden, müssten wir in dieser VM auch jedes mal einen neuen Schlüssel generieren. Das klappt nur leider nicht, da der Public Key immer ein anderer wäre!

Deshalb haben wir unseren Key vorsorglich auf unserem Server erstellt und dort den Public Key bereits als authorized_key eingetragen. Verwenden wir nun immer den selben Private Key, passt dieser auch immer zum Public Key auf dem Server und unsere Build Pipeline kann sicher und vor allem passwortlos unser Build Artifact auf den Server übertragen.