🗾 Gitea Actions Teil2 - Jekyll-Dockerimage

11 Minute(n) Lesezeit

Die Vorgeschichte

Leider hatte ich im Teil1 mit einer leicht modifizierten Kopie der Github-Action zum Bauen von Jekyll kein GlĂŒck. Daher wollte ich nun probieren, Jekyll direkt als Dockerimage laufen zu lassen. Im Docker hub gab es nicht so furchtbar viel Auswahl. Trotzdem habe ich ein meiner Meinung nach aktiv gewartetes und gut dokumentiertes Image finden können: jvconseils jekyll-docker.

“Set up job”

Und so binde ich das Docker-Jekyllimage dann in meinen workflow ein:

# workflows/jekyll-build-action.yml
jobs:
  # Build job
  build:
    runs-on: ubuntu-latest # this is the "label" the runner will use and map to docker target OS
    container: jvconseil/jekyll-docker
# [...]

Hier wird dem gitea act_runner gesagt, dass er mit dem ubuntu-latest label laufen soll, was in meinem Fall auf eine sehr kleine und schnell hochzufahrende node16:bullseye Maschine (abgeleitet vom Betriebssystem Debian11) gemappt wird. ZusĂ€tzlich wird Docker mitgeteilt, dass bitte das jekyll-docker image zu laden ist, wo die von mir benötigten AbhĂ€ngigkeiten (Ruby, Sass, Jekyll
) bereits berĂŒcksichtigt wurden.

bundle install

Das Bauen schlÀgt hier allerdings fehl wegen in der Gemfile angegebenen, aber nicht installierten AbhÀngigkeiten. Das kann dann so Àhlich aussehen wie hier:

bundler: failed to load command: jekyll (/usr/gem/bin/jekyll)
/usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:332:in `raise_not_found!': Could not find gem 'github-pages' in locally installed gems. (Bundler::GemNotFound)
	from /usr/local/lib/ruby/gems/3.2.0/gems/bundler-2.4.22/lib/bundler/resolver.rb:392:in `block in prepare_dependencies'

Bundler ist der Paketmanager fĂŒr in Ruby geschriebene Software. Ähnlich wie npm fĂŒr Javascript oder pip fĂŒr Python ist bundler in der Lage, von einem Ruby-Programm genutzte Bibliotheken und andere AbhĂ€ngigkeiten herunterzuladen und so einzubinden, dass sie ohne weitere Konfiguration auf dem Zielsystem verwendet werden können. Damit bundler weiß, was zu installieren ist, wird eine sogenannte Gemfile angelegt.

Also dem Script schnell bundle install hinzufĂŒgen, etwa so:

# workflows/jekyll-build-action.yml
# [...]
    steps:
      - name: --- CHECKOUT ---
        uses: actions/checkout@v3
      - name: --- INSTALL GEMS ---
        run: bundle install # will fail with permissions rights to write to Gemfile.lock but anyways installs required dependencies.
      - name: --- BUILD WITH JEKYLL ---
        # Outputs to the './_site' directory by default
        run: bundle exec jekyll build --destination /opt/blog_staging
        env:
          JEKYLL_ENV: production

act_runner: Kein Schreibzugriff auf Gemfile.lock

Dies schlÀgt wieder fehl - wegen fehlender Schreibrechte des act_runner:

gitea-runner-1  | [Deploy Jekyll site/build]   | `/workspace/schallbert/blog/Gemfile.lock`. It is likely that you need to grant
gitea-runner-1  | [Deploy Jekyll site/build]   | write permissions for that path.
gitea-runner-1  | [Deploy Jekyll site/build]   ❌  Failure - Main ---INSTALL GEMS ---
gitea-runner-1  | [Deploy Jekyll site/build] exitcode '23': failure

Nach langer Suche muss ich feststellen, dass Gemfile.lock auf meinem Rechner nicht der Versionskontrolle unterliegt, da es in der .gitignore auftaucht. Der Bundler des act_runner versucht nun also, diese Datei selbst aus der Gemfile zu erzeugen und scheitert, weil keine Schreibrechte auf dem Repository bzw. der temporÀren Kopie vorliegen.

Dies lĂ€sst sich am besten beheben, indem die Gemfile.lock in die Versionskontrolle aufgenommen wird, d.h. aus der .gitignore verschwindet. Somit stelle ich sicher, dass ich fĂŒr das lokale Bauen auf meinem Laptop dieselben Umgebungsbedingungen habe wie auf dem Server.

Ist dies nicht gewĂŒnscht, so kann man dem zugreifenden User Bundler auch den Besitz der Datei ĂŒbergeben (chown Bundler Gemfile.lock), Hintergrundinformationen dazu sind auf der Bundler Website zu finden.

AbhÀngigkeiten: sass-embedded

Nach weniger als 20 Sekunden die nÀchste Fehlermeldung:

Resolving dependencies...
Could not find gem 'sass-embedded (= 1.69.5)' with platform 'x86_64-linux' in
rubygems repository https://rubygems.org/ or installed locally.

Nach weiteren Minuten der Recherche die Lösung: In der Gemfile eine bestimmte Version des jekyll-sass-converter spezifizieren oder alternativ das Gem “github-pages” in der Gemfile belassen. Dieses kĂŒmmert sich ebenfalls darum, dass der CascadingStyleSheets (CSS) PrĂ€prozessor Sass vorhanden ist.

Nun endlich lÀuft bundle install erfolgreich durch:

✅ Bundle complete! 5 Gemfile dependencies, 43 gems now installed.

jekyll build

Damit Jekyll die gebauten Dateien auch irgendwo abspeichern kann, habe ich dem act_runner in der docker-compose.yml ein weiteres Volume gegönnt:

# gitea/docker-compose.yml
# [...]
volumes:
      - ./runner/blog_staging:/opt/blog_staging
      - ./runner/data:/data
      - /var/run/docker.sock:/var/run/docker.sock

Und jetzt direkt nochmal probieren, ob Jekyll erfolgreich baut:

       Destination: /opt/blog_staging
      Generating... 
       Jekyll Feed: Generating feed for posts
jekyll 3.9.3 | Error:  Permission denied @ dir_s_mkdir - /opt/blog_staging
/usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `mkdir': Permission denied @ dir_s_mkdir - /opt/blog_staging (Errno::EACCES)
	from /usr/local/lib/ruby/3.2.0/fileutils.rb:406:in `fu_mkdir'

Auch dieses Problem lÀsst sich leicht lösen, indem man entweder nach /tmp baut, wo der user jekyll wÀhrend des Bauens ebenfalls Zugriff hat, oder indem man der Action vor dem jekyll build-Befehl einen Nutzerwechsel im Build-Arbeitsverzeichnis vornimmt: chown -R jekyll /opt/blog_staging.

Mit dieser Änderung lĂ€uft die Action endlich durch. Image: Gitea snapshot showing a successful build

Artefakte transferieren

Doch wo finde ich jetzt die in der Action gebauten Dateien, welche mein Webserver dann veröffentlichen soll? Der _site-Ordner ist nirgendwo zu finden - weder auf dem Volume der Hostmaschine, noch im Gitea-Dockercontainer oder im Runner!

Docker volumes fĂŒr act_runner können Artefakte nicht verteilen

Dass ich ĂŒber Docker volumes nicht dort herankomme erscheint logisch wenn man weiß, wie act-runner funktioniert: Die Action erzeugt ein eigenes Dockerimage mit wiederum per Task-ID assoziierten Volumes, welches nur fĂŒr dessen Laufzeit existiert.

DRIVER    VOLUME NAME
--> local     GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build <--
--> local     GITEA-ACTIONS-TASK-84_WORKFLOW-Deploy-Jekyll-site_JOB-build-env <--
local     act-toolcache
local     blog_staging

Legt man die Artefakte also nirgendwo extern ab, so werden sie nach Durchlauf der Aktionen zusammen mit dem Image demontiert und sind futsch. Das oben angelegte Volume des Runners hat gar keine Auswirkung auf die Action, weil sie wieder in einem isolierten Image lÀuft.

Dies zu verstehen hat mich einige Zeit und Fehlversuche gekostet, dabei ist es in einem Abschnitt der Dokumentation beschrieben.

Das Ganze gestaltet sich ja deutlich komplizierter als ich dachte. Mit Kanonen auf Spatzen schießen angesichts meiner kleinen Website und als Einziger, der sie pflegt. Egal, ich ziehe das jetzt durch.

Aber man kann doch upload-artifact verwenden!

Na ja, das dachte ich jedenfalls. Also habe ich mein Action-Skript entsprechend erweitert und die von Github Actions zur VerfĂŒgung gestellte Funktion eingebaut:

# workflows/jekyll-build-action.yml
# [...]
# Automatically upload the build folder to Giteas blog repo folder
      - name: --- UPLOAD ARTIFACT ---
        uses: actions/upload-artifact@v3
        with:
          path: /workspace/schallbert/blog/
          name: Blog_Staging
          retention-days: 2

Super, jetzt erscheint ein herunterladbares Zip-file auf der WeboberflÀche von Gitea:

Image: Gitea snapshot showing a successful artifact upload

Nur: Wie bekomme ich das Ding jetzt automatisiert auf meine Serverinstanz? Schließlich kennt die Action diese nicht - sie hat ja nicht einmal einen eigenen Docker Daemon.

Also schaue ich mal im Gitea-Container selbst nach, wo die Artefakte unter /data/gitea/actions_artifacts/BUILD_ID abgelegt sind. Allerdings als ein Haufen (hunderte) .chunk.gz Dateien mit kryptischen Nummernfolgen als Name, bei denen ich wiederum nicht weiß wie ich sie zu einem Archiv zusammenfĂŒgen soll.

Und noch etwas gefĂ€llt mir an upload-artifact nicht: Da meine Website inzwischen eine Menge Bilder, Audiodateien und Videos enthĂ€lt, ist sie recht groß. Daher benötigt der uploader fĂŒr’s Verpacken und Versenden schon jetzt fast zwei Minuten - Tendenz linear steigend mit der Anzahl meiner Medieninhalte.

Sackgasse.

Auch Docker cp funktioniert in diesem Falle nicht

Dann versuche ich mit Hilfe des Kopierbefehls von Docker, die Artefakte auf meinen Host zu transferieren:

# workflows/jekyll-build-action.yml
# [...]
- name: --- COPY ARTIFACT ---
        run: docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging

Dies schlÀgt wieder fehl:

gitea-runner-1  | docker cp gitea-runner-1:/workspace/schallbert/blog /tmp/blog_staging
[...]
gitea-runner-1  | [Deploy Jekyll site/build]   | /var/run/act/workflow/3.sh: line 2: docker: not found
gitea-runner-1  | [Deploy Jekyll site/build]   ❌  Failure - Main --- COPY ARTIFACT ---

Auch erwartbar, denn der Action-Container selbst kennt kein Docker. Es ist also nicht möglich, etwas von “innerhalb” des Containers per Docker heraus zu bekommen. Dies ist fĂŒr mich aber erforderlich, da die Action ja den ganzen Prozess vollstĂ€ndig automatisieren soll.

Oder geht es doch irgendwie? Der Daemon der Hostmaschine docker.sock steht laut Giteas Dokumentation zumindest dem act_runner per Volume zur VerfĂŒgung - wird aber scheinbar nicht in die Action weitergeleitet. Vielleicht aus gutem Grund, sonst wĂ€re die Kapselung futsch und eine schadhafte Action könnte praktisch meinen Docker Daemon auf dem Host kapern 😰.

Transferieren per SFTP wĂŒrde klappen

So langsam gehen mir die Alternativen aus. Ich ĂŒberlege nun, die Artefakte per SFTP aus dem vom act_runner erzeugten Action-Dockerimage heraus in den Webserver-Container zu schieben1.

Image: Machine architecture if I used SFTP for artifact share between Gitea and webserver

GlĂŒcklicherweise gibt es hierfĂŒr wieder eine vorgefertigte Action: scp-files. Nun muss ich aber den Container des Webservers so umbauen, dass er per SCP erreichbar ist und den SSH-SchlĂŒssel der Action akzeptiert.

Andererseits wird davor in vielen Foren gewarnt - Ein Container sollte nur eine Applikation beherbergen. Aber auch hier gibt es eine Lösung: ein eigener Docker-Container mit SSH-Daemon. Diesem könnte ich dann ein Volume geben, welches er mit dem Webserver teilt.

Sieht leider nach einigem Aufwand aus. Und ich möchte als fauler Entwickler mein System so einfach wie möglich halten - also recherchiere ich weiter.

Dem Action-Container ein Volume zur VerfĂŒgung stellen

Image: Docker-Gitea infrastructure overview

Diese ganzen FehlschlĂ€ge und Sackgassen begleiten mich jetzt schon seit ĂŒber einem Monat. Ich will es trotzdem unbedingt schaffen, vollautomatisch bauen, testen und veröffentlichen zu können. Auf dem Weg hierhin habe ich eine Menge gelernt und jetzt hoffe ich, dass dieser Ansatz mich endlich zur Lösung bringt.

Bei meiner Suche stieß ich auf die Möglichkeit, die Konfiguration des Runners zu verĂ€ndern. So könnte ich am Ende vielleicht doch noch ein Volume einbinden, welches ich zwischen der Action und meinem Server ĂŒber den Daemon auf dem Host teilen kann.

Wenn das klappt, hĂ€tte ich kein zusĂ€tzliches Sicherheitsrisiko wie ein öffentlich verfĂŒgbarer file transfer container oder die langen Wartezeiten durch upload-artifact. Also frisch ans Werk! Es gibt noch weitere GrĂŒnde, weswegen Volumes in der jeweiligen Action Sinn machen können - es muss also bereits Leute da draußen geben, die das hinbekommen haben.

Ich versuche also, die Volumes im Action-Skript anzulegen:

# workflows/jekyll-build-action.yml
# [...]
container: 
      image: jvconseil/jekyll-docker:latest
      volumes: 
        - /tmp/blog_staging:/blog_staging
        - /opt/cache:/opt/hostedtoolcache

Doch irgendwie landen die Artefakte noch immer nicht auf der Platte meines Hosts. Ich vereinfache die Action soweit, dass ich nur noch prĂŒfe, ob die Erzeugung des Volumes auch einen entsprechenden Ordner anlegt:

# workflows/jekyll-build-action.yml
# [...]
steps:
  - name: --- CHECK_VOLUME ---
  run: |
    ls -al /blog_staging

und schon schlÀgt die Aktion wieder fehl:

gitea-runner-1  | [Deploy Jekyll site/build] [DEBUG] Working directory '/workspace/schallbert/blog'
gitea-runner-1  | [Deploy Jekyll site/build]   | ls: /blog_staging: No such file or directory
gitea-runner-1  | [Deploy Jekyll site/build]   ❌  Failure - Main --- CHECK_VOLUME ---

Nach ein paar weiteren, erfolglosen Versuchen (vielleicht hatte ich etwas falsch geschrieben?) und langer Recherche in den Foren fand ich heraus, dass es das valid_volumes Attribut im act gibt - taucht das Volume dort nicht auf, so wird es auch nicht eingebunden.

OK, also schnell der Action hinzugefĂŒgt:

# jekyll-build-action.yml
# [...]
container: 
      image: jvconseil/jekyll-docker:latest
      valid_volumes: 
        - '**'  # This does not work. Also specialized lists indicating the volumes directly won't work
      volumes: 
        - /tmp/blog_staging:/blog_staging
        - /opt/cache:/opt/hostedtoolcache

Aber immer noch nichts. Immerhin kann ich in den Logs mit dem Wissen ĂŒber valid_volumes jetzt eine Warnung entdecken:

gitea-runner-1  | [Deploy Jekyll site/build] [/tmp/blog_staging] is not a valid volume, will be ignored
gitea-runner-1  | [Deploy Jekyll site/build] [/opt/cache] is not a valid volume, will be ignored

Etwas stimmt mit der Übergabe dieser Option nicht. Also recherchiere ich weiter. Es gibt Berichte, in denen das Anmelden der Volumes funktioniert. Also erzeuge ich nach Anleitung eine Konfigurationsdatei, wo ich das Folgende eintrage:

# runner/config.yml

# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
  # You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
  # For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
  # valid_volumes:
  #   - data
  #   - /src/*.json
  # If you want to allow any volume, please use the following configuration:
  # valid_volumes:
  #   - '**'
  valid_volumes: ["/tmp/blog_staging", "/opt/hostedtoolcache"]

Wichtig: Die Notation muss als String vorliegen, kommasepariert, und gibt stets die Source des Volumes an. Also den Teil, der vor dem : steht. Doch noch immer erscheint beim Bauen die Fehlermeldung “not a valid volume”.

Doch dann fĂ€llt mir auf, dass die Config dem act_runner selbst als Volume zur VerfĂŒgung gestellt werden muss - sonst kann der im Container laufende Runner ja gar nicht darauf zugreifen!

Daher sieht meine docker-compose.yml fĂŒr Gitea, Abschnitt “runner” nun wie folgt aus:

# gitea/docker-compose.yml
# [...]
  runner:
    image: gitea/act_runner:latest
    environment:
      - CONFIG_FILE=/config.yml
      - GITEA_INSTANCE_URL=https://git.schallbert.de
      - GITEA_RUNNER_NAME=ichlaufe
      - GITEA_RUNNER_REGISTRATION_TOKEN= <redacted>
    volumes:
      - ./runner/config.yml:/config.yml
      - ./runner/data:/data
      - /opt/hostedtoolcache:/opt/hostedtoolcache
      - /var/run/docker.sock:/var/run/docker.sock

Und endlich:

gitea-runner-1  | ls -al /blog_staging
gitea-runner-1  | [Deploy Jekyll site/build]   | total 8
gitea-runner-1  | [Deploy Jekyll site/build]   | drwxr-xr-x    2 root     root          4096 Dec 27 07:32 .
gitea-runner-1  | [Deploy Jekyll site/build]   | drwxr-xr-x    1 root     root          4096 Dec 27 07:46 ..
gitea-runner-1  | [Deploy Jekyll site/build]   ✅  Success - Main --- CHECK_VOLUME ---

Was fĂŒr ein Akt. Ich bin so froh, dass jetzt alles durchlĂ€uft und ich auf meinem lokalen Hostsystem tatsĂ€chlich die gebauten Dateien vorfinde! đŸ„ł

Noch ein letzter Tipp

Wenn ihr so wie ich mit einem Reverse Proxy arbeitet und so komische connection refused Fehlermeldungen beim Hochfahren des Runners erhaltet wie:

docker compose up
[+] Running 2/0
 ✔ Container gitea           Created                                                                                                                     0.0s 
 ✔ Container gitea-runner-1  Created                                                                                                                     0.0s 
Attaching to gitea, gitea-runner-1
gitea-runner-1  | level=info msg="Starting runner daemon"
gitea-runner-1  | level=error msg="fail to invoke Declare" error="unavailable: dial tcp <address>: connect: connection refused"
gitea-runner-1  | Error: unavailable: dial tcp <address> connect: connection refused
gitea           | Server listening on :: port 22.
gitea-runner-1 exited with code 1

Dann ist euer Reverse Proxy entweder falsch eingestellt oder - wie in meinem Falle - nicht hochgefahren.

  1. Die Nutzung des SFTP (Secure File Transfer Protocol), also der Dateitransfer ĂŒber ssh (secure shell), erhöht die AngriffsflĂ€che meines Systems ein wenig. Der SSH-Dienst ist nĂ€mlich dann nicht nur fĂŒr meine Action, sondern ganz allgemein von außen erreichbar.