🗸 Gitea Actions Teil2 - Jekyll-Dockerimage
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.
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:
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.
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
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.
-
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. ↩