CI/CD mit Hugo

Als Admin hat man meist weniger mit den Werkzeugen der Softwareentwicklung zu tun, abgesehen von deren Installation, Updates und ggf. Wartung. Wie aber genau funktionieren solche CI/CD-Ketten, was kann man damit machen und was bringt mir denn sowas? Ich habe mal den Blick über den Tellerrand gewagt und habe gefallen daran gefunden.

Um an der Stelle mal fefe (sinngemäß) zu zitieren: "Was Du mehr als dreimal machst automatisiertst Du." Daran musste ich immer wieder beim Arbeiten mit hugo denken: Seite/Post erstellen, übersetzen, auf den Webserver hochladen und dabei das ganze dann auch noch im git aktuell halten. Also warum nicht einfach mal damit eine CI/CD-Pipeline bauen?

Was brauch ich denn alles so dafür?

Das wichtigste Werkzeug für das Vorhaben ist eine CI/CD-Software, nehmen wir also mal den Quasistandard Jenkins. Die Installation und Konfiguration ist ausführlich dokumentiert, das lasse ich mal bei Seite. Weiter brauch ich noch mein git (das läuft sowieso, ich benutze gitblit), einen Editor (vim, Atom, etc.) und natürlich hugo. hugo lass ich bei mir in einem Dockercontainer laufen. Nachdem die Software soweit bereitstand und funktionierte hab ich mich in die Jenkins-Dokumentation gestürzt...

Wie funktionierts?

Um im Jenkins nicht allzuviel konfigueren zu müssen verwende ich die Jenkinspipelines welche mit einen "Jenkinsfile" gesteuert werden. In diesem Fall reicht es, wenn ich in meine git-Repo einfach das Jenkinsfile mitliefere um den gesamten Prozess zu steuern. Demnach muss ich nur einen Job anlegen in dem das Repo ausgecheckt wird und danach das Jenkinsfile eingelesen wird - klingt spanend. Die Konfiguration eines Jobs in Jenkins ist auf anderen Seiten bestens dokumentiert, deshalb lasse ich das auch mal weg. Daraus ergibt sich dann auch der Arbeitsablauf:

  • Seite/Post erstellen oder bearbeiten
  • ins git pushen
  • Jenkins seine Arbeit machen lassen
    • pullen
    • Jenkinsfile ausführen
    • Seite mit hugo in einem Dockercontainer bauen
    • Seite hochladen
    • Mail schreiben
  • Fertig

Ja, ich weiß, da fehlt noch eine ganze Menge und Jenkins muss auch noch docker ausführen dürfen, aber für den Anfang ist das OK.

Das Jenkinsfile

Mein Jenkinsfile wollte ich erst einmal klein halten und nur mit dem nötigsten ausstatten. Also habe ich kurz überlegt, welche Schritte ich in der Pipeline benötige. Der Einfachheit halber nehme ich mal den klassischen "Dreisatz":

  • Build
  • Test
  • Deploy

Abschließend kommt noch ein "Post" für die Mailbenachrichtigung.

pipeline {
    agent none
    stages {
        stage('Build') {
          agent {
            docker {
              image 'klakegg/hugo'
              args '-ti -v $WORKSPACE/SOURCE/PATH:/src --entrypoint=""'
              }
        }
            steps {
                echo 'Building...'
                sh 'rm -rf src/public/*'
                sh 'cd /src && hugo -v'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing...'
            }
        }
        stage('Deploy') {
          agent any
            steps {
                echo 'Deploying...'
                sh "ssh USER@wSERVER -a /WWW/ROOT /BACKUP/WWW/ROOT"
                sh "scp -r $WORKSPACE/SOURCE/PATH/public/* USER@SERVER:/DESTINATION/PATH"
            }
        }
      }
    post {
      success {
        emailext (
          subject: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",
          body: """<html>
                    <head>
                    </head>
                    <body>
                      <p>SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
                      <p>Check console output at Job &quot;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&quot;</p>
                    </body>
                    </html>""",
          mimeType: 'text/html',
          recipientProviders: [[$class: 'DevelopersRecipientProvider']]
        )
      }
      failure {
        emailext (
          subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",
          body: """<html>
                    <head>
                    </head>
                    <body>
                      <p>FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>
                      <p>Check console output at Job &quot;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&quot;</p>
                    </body>
                    </html>""",
          mimeType: 'text/html',
          recipientProviders: [[$class: 'DevelopersRecipientProvider']]
        )
      }
    }
}

Im Build-Step musste ich für den Dockercontainer den "entrypoint" überschreiben um dann sinnvoll die Befehle im Container ausführen zu können. Hier werden erst einmal die Inhalte des "public"-Verzeichnisses gelöscht und dann mit hugo selbige neu erstellt. Das testen habe ich vorerst übersprungen, da ich noch keine Idee habe was ich da wie machen kann und soll. Im Deploy-Step verwende ich einen anderen "agent" und erstelle auf dem Zielserver vor allem anderen ein Backup (man kann ja nie wissen). Anschließend kopiere ich die Übersetzten Inhalte in mein Zielverzeichnis auf dem Server. Einen rsync könnte man da auch machen, da aber meine Seite noch nicht so groß ist hab ich scp genommen. In dem "post" führe ich nur E-Mailbenachrichtigungen aus, wenn die Pipeline erfolgreich abgearbeitet wurde oder wenn es zu eine Fehler kommt.

Nach ca. 90 Jenkinsbuilds (try & error) habe ich das Jenkinsfile soweit dass erst einmal die grundlegenden Funktionen soweit am Start sind und ich meinen Arbeitsablauf wie oben beschrieben durchführen kann. Wenn diese Seite dann online ist, hat das ganze dann auch funktioniert.

Was hats mir gebracht?

Ich kann jetzt relativ einfach Blogposts ergänzen ohne mich mit diversen automatisierbaren Routinen aufhalten zu müssen. Trotz allem lasse ich immer einen "hugo server" mitlaufen um mir die Ergebnisse meiner Arbeit direkt im fertigen Zustand ansehen zu können. Ich habe auch einen kleinen Einblick in einen Teil der Arbeitsweise von Softwareentwicklern bekommen (man sollte das auch als Admin immer wieder mal machen), der mir einiges an Verständniss gebracht und Lust auf Mehr gemacht hat.

Ausblicke

In jedem Fall werde ich mich als nächstes mal um das "staging" mit Hilfe der Möglichkeiten von git kümmern. Vermutlich werde ich ein DEV- und/oder STAGE-Branch erstellen. Letzterer dann wird mit/über Jenkins auf diverse Qualitätsmerkmale geprüft und kann dann nach Bestehen wieder in den Master gemerged werden, aus dem dann die finale Webseite gebaut wird. In diesem Zuge werde ich das Testing angehen und mal schauen wie man überhaupt und auf was man so alles automatisiert testen kann.