Kontakt: +49 511 59095 – 942

Cloud Arbeiterbienen für die Build Pipeline - Jenkins mit dynamischen Verarbeitungsknoten über AWS Plugin

Thumbnail

Jenkins als Build Server erfreut sich einer großen Verbreitung. Die architekturelle Frage, die sich dabei stellt ist: Wie groß lege ich den Server aus, damit er neben des Management der Build Projekte auch die Builds selber verarbeiten kann?

Die einfachste Antwort darauf ist ja: AWS CodeBuild verwenden, aber wenn das nicht geht?

Dann bietet das EC2-Plugin eine dynamische Lösung mit AWS Mitteln: Die Worker Knoten werden vollautomatisch dynamisch als EC2 Instanzen erzeugt und wieder abgebaut. So kann einfach eine kostengünstige Lösung aufgebaut werden.

Die Dokumentation zu diesem Plugin beinhaltet kein durchgängiges Beispiel. Daher will ich hier die komplette Installation eines Jenkins mit dynamischen Arbeitsknoten (worker nodes) auf AWS darstellen. In ca. 12 Stunde steht alles! Zur Automatisierung habe ich die automatisierbaren Arbeitsschritte in ein Makefile eingebaut. Der Code und die Scripte sind auf github verfügbar: TecRacer Github Jenkins demo  

Unsere Zutaten

Wir brauchen:

  1. Eine Rolle für die Jenkins Server Instanz
  2. Eine Security Group für den Server
  3. Eine Security Group für die Knoten
  4. Einen Jenkins Server ( im Beispiel auf einer EC2 Instanz)
  5. Ein Script, um Java zu installieren
  6. Das EC2 Plugin
  7. Die Konfiguration für das Plugin

In der Rolle wird dem Server z.B. erlaubt andere Instanzen zu starten und zu stoppen. Mit den beiden Security Groups wird auf Port Ebene die Verbindung erlaubt. Im Beispiel läuft die Verbindung unsicher über öffentliche IP Adressen.  

Installation

Vorbereitung für die Instanz: Rollen und Gruppen

Die notwendigen Resourcen sind in templates/jenkins-security.template kodiert. Also zuerst diese Datei zu cloudformation hochladen. Entweder manuell über die AWs Konsole oder wenn man clouds aws (siehe Blogpost dazu) installiert hat, einfach mit make security die Aufgabe aus dem Makefile des github Repositories aufrufen. Dabei eventuell im Makefile die Region anpassen, Standard ist eu-central-1 (Frankfurt).

Ein Jenkins Server auf EC2

Wer schon einen Server hat, kann diesen Schritt natürlich überspringen. Weil ich hier ein AMI aus dem Marketplace verwendet, bauen wir den Server manuell auf. In der Konsole

  1. Service EC2 aufrufen
  2. Launch Instance aufrufen
  3. Links “AWS Marketplace” wählen
  4. bitnami jenkins suchen
  5. “Jenkins Certified by Bitnami” auswählen
  6. Auf der Übersichtsseite names “Jenkins Certified by Bitnami” den Punkte “continue” auswählen
  7. Instanztype wählen, z.B. t3.small
  8. Bei “Configure Instance Details”
    • Das vpc wählen, das in den cloudformation Parametern eingetragen ist. (Kleine Hilfe: make show-vpc) Automatisch ausgewählt ist das Standard VPC
    • Ein public subnet und eine öffentliche IP wählen, sonst haben wir keinen Zugang zu der jenkins WebGui
    • Als IAM Rolle JenkinsRole wählen
  9. Bei “Step 6: Configure Security Group”:
    • “Select an existing security group” die Gruppe mit der Beschreibung “Jenkins Server” wählen, die durch das Cloudformation Template eben angelegt wurde
  10. Key Pair brauchen wir für die Demo selber nicht. Wer sich einloggen will: der ec2-user ist hier “ubuntu”
  11. Launch Instance

Konfiguration des Jenkins Servers

Der Server Administrator hat den Usernamen user. Das Kennwort wird dynamisch generiert. So ermittelt man das Kennwort:

Kennwort ermitteln

Wenn der Server hochgefahren ist und die Status Checks durch sind:

  1. Wähle Actions -> Instance Settings -> Get System Log
  2. Da sucht man jetzt nach:

    #########################################################################
    #                                                                       #
    #        Setting Bitnami application password to 'Ki2drNVDDT0b'         #
    #        (the default application username is 'user')                   #
    #                                                                       #
    #########################################################################
    

    (Das hier gezeigt password ist kein echtes, nur ein Beispiel!)

Auf der Shell der Instanz wäre das:

sudo grep "application password" /var/log/syslog

Jenkins verwenden und das EC2 Plugin konfigurieren

Jetzt kann man sich unter der öffentlichen IP mit dem user und Kennwort aus dem System log anmelden. Achtung: Die Daten gehen unverschlüsselt über das Internet, das ist nur für eine Demo ok, auf keinen Fall für eine länger laufende Instanz. Jetzt wählt man “install selected plugins” und startet den Server einmal neu.

EC2 Plugin installieren

  1. Zum Plugin Manager gehen: baseURL/jenkins/pluginManager/
  2. “Available” anwählen
  3. Auf EC2 filtern
  4. “Amazon EC2” anwählen
  5. Install without restart wählen

Navigation zum Plugin Manager über die GUI:

ec2-plugin

Und GUI Plugin wählen:

available-plugins-ec2-jenkins

Das Plugin “Amazon EC2 Plugin” wählen.

Plugin Konfigurieren

Unter “Jenkins/Konfiguration” (basURL/jenkins/configure) “Cloud hinzufügen”:

ec2-plugin-step1

Die Konfiguration

  1. Name (Beispiel): Arbeitsbienen
  2. Use EC2 instance profile to obtain credentials: anwählen
  3. Region: eu-central-1
  4. EC2 Key Pair’s Private Key: Hier einen private SSH Key einkopieren, der unter AWS verfügbar ist. Kann man unter Services-EC2-Network&Security-Key Pairs generieren
  5. Test Connection klicken. Wenn “Success” angezeigt wird, ist der erste Schritt ok ec2-plugin-step2-

Jetzt muss noch der Worker Node eingestellt werden. Das ist der etwas komplexere Teil:

AMI konfigurieren

  1. Description: LinuxWorkerBee (Beispiel)
  2. AMI: ami-09def150731bdbcc2
  3. Check AMI klicken, AMI muss angezeigt werden, das Beispiel AMI ist ein aktuelles Linux2 AMI für eu-central-1
  4. Instance Type: T3Micro (Beispiel)
  5. Availability Zone: eu-central-1b
    Die AZ hängt natürlich von eurem VPC und Subnets ab, mit dem Standard VPC sollte das aber so funktionieren
  6. Security group names: Security Group ID der SG SecurityGroupJenkinsNode. Wird auch abgefragt im Beispiel mit make show-sg.
  7. Remote User: ec2-user
  8. Subnet ID: die Subnet ID muss muss mit der Availability Zone zusammen passen.
  9. Eine Liste dazu bekommt man hier mit make show-subnet

Unter Userdata wird das JDK installiert. Es wird für den Jenkins Agenten auf dem Knoten verwendet:

#!/bin/bash -xe 
sudo yum -y install java-1.8.0 
sudo yum -y remove java-1.7.0-openjdk

In der Übersicht sollte das in etwa so aussehen:

overview

Dann “Hinzufügen” wählen. Zum Schluss “Speichern” wählen. Wenn alles stimmt, haben wir jetzt Arbeitsknoten. Auf zum Test.

Test des Plugins

Build Projekt im Jenkins Server anlegen

  1. Neues Projekt anlegen:

  2. Name: Demo (Beispiel)

  3. Typ: “FreeStyle” Softwareprojekt bauen

  4. Buildverfahren: Shell ausführen

  5. Befehl: sleep 300
    Hier brauchen wir nur einen Befehl, der die Knoten länger beschäftigt…

So in etwa soll das aussehen:

Und

Schritt5

Build ausführen

Wenn wir jetzt einen Build starten:

7

Dann wird der auf unserem Jenkins Server ausgeführt, noch nicht auf dem dynamischen Knoten.

Warum? Das EC2 Plugin startet erst Nodes, wenn der Server überlastet ist. Überlastung ist definiert als mehr gleichzeitige Builds, als der Server verarbeiten kann. Daher stellen wir zum Test ein, dass unser Jenkins Server gar keine Builds verarbeiten kann:

ec2-plugin-step6

Jetzt starten wir den Build erneut. Und es passiert…erstmal nichts. Wir müssen noch etwas in den erweiterten Konfigurationen des EC2 Plugins einstellen: Dazu:

  1. Wählen wir “erweitert:

    ec2-plugin-step10

  2. Stellen für “Instance Cap” z.B. 10 ein. Das ist die Anzahl der builds, die der Worker node verarbeiten kann

  3. No delay provisioning: Enabled
    Diese Einstellung steuert, das der Jenkins Server nicht nach eine internen (mir unbekannten) Algorithmus die Worker nodes startet, sondern sofort, wenn mehr Arbeit da ist, also der Server und eventuelle andere Knoten verarbeiten kann

  4. Speichern!

Das sollte in etwa so aussehen:

ec2-plugin-step11

Um zu schauen, ob die dynamische Provisionieren jetzt klappt, schauen wir uns nochmal an, wieviele EC2 Instanzen gerade laufen:

ec2-plugin-list-instances-before

Build automatisch

Start Prozess des Knotens

Nach dem Start des Builds steht er erst in der Warteschleife:

ec2-plugin-run-step1

Dann wird ein Arbeits-Knoten gestartet:

ec2-plugin-run-step2

Nach dem Provisionieren des Knotens steht die eingestellt Anzahl an Buildern bereit und das Demo Projekt wird gestartet:

ec2-plugin-run-step3

Und auch auf AWS Seite kann ich die Arbeitsknoten sehen:

ec2-plugin-run-step4

Was passiert beim Starten des Knotens im Detail?

  1. Instanz starten jund provisionieren:

        May 05, 2019 11:10:36 AM hudson.plugins.ec2.EC2Cloud
        INFO: Launching instance: i-093802e00d601dc28
    
  2. Server verbindet sich per ssh:

    INFO: Connecting to ec2-52-29-233-168.eu-central-1.compute.amazonaws.com on port 22, with timeout 10000.
    INFO: Connected via SSH.
    
  3. Java Packete installieren

  4. Ausführungs Jar “slave” installieren:

    May 05, 2019 11:11:06 AM hudson.plugins.ec2.EC2Cloud INFO: Copying slave.jar to: /tmp
    
  5. Agenten starten:

    INFO: Launching slave agent (via Trilead SSH2 Connection):  java  -jar /tmp/slave.jar
    ...
    Agent successfully connected and online
    

Dann wird der Knoten nach der eingestellten “Ruhezeit” idle time wieder runtergefahren. Die komplette Konfiguration (siehe Anhang) kann man vom Jenkins Server in der “config.xml” sehen. Bei Bitname ist das im Beispiel in

/opt/bitnami/apps/jenkins/jenkins_home

.

Worauf ist noch zu achten

Bei dynamischen Erzeugen muss man beachten, dass es für das Starten von EC2 Instanzen Soft Limits gibt. Diese sind eventuell über den AWS Support hochzusetzen.

Für bestimmte Builds kann auch der Einsatz von Docker Containern hilfreich sein. Dafür gibt es ein anderes Jenkins AWS Plugin.

Fazit

Ist man auf Jenkins festgelegt, kann man dennoch mit dem EC2 Plugin dynamisch Arbeitsknoten erzeugen. Dabei ist die Konfiguration relativ komplex, man kann viel falsch machen, wenn z.B. Subnet und Region nicht stimmen, oder die Security Groups nicht passen usw.. Unter dem Aspekt der Sicherheit müsste man alles in private Subnetze umbauen und den Jenkins mit https versehen.

Außerdem muss man den Jenkins Server patchen und verwalten. Keine Sorgen um dynamisches Provisionieren von Knoten muss man sich bei der Verwendung des AWS Server CodeBuild machen. Eine komplette Anwendung mit Codepipeline lässt sich mit CodeStar einrichten.

Der Werbeblock: Dabei unterstützt tecRacer natürlich gerne!

Es ist also abzuwägen, welche Lösung für Ihre Architekturanforderunge die passende ist.

Anhang

Jenkins Beispiel Konfiguration

<clouds>
   <hudson.plugins.ec2.ec2cloud plugin="ec2@1.42">
      <name>ec2-Arbeitsbienen</name>
      <useinstanceprofileforcredentials>true</useinstanceprofileforcredentials>
      <rolearn />
      <rolesessionname />
      <credentialsid />
      <privatekey>
         <privatekey>...</privatekey>
      </privatekey>
      <instancecap>10</instancecap>
      <templates>
         <hudson.plugins.ec2.slavetemplate>
            <ami>ami-09def150731bdbcc2</ami>
            <description>LinuxWorkerBee</description>
            <zone>eu-central-1c</zone>
            <securitygroups>sg-0f0b7d051f93b8296</securitygroups>
            <type>T3Micro</type>
            <ebsoptimized>false</ebsoptimized>
            <monitoring>false</monitoring>
            <t2unlimited>false</t2unlimited>
            <labels />
            <mode>NORMAL</mode>
            <userdata>#!/bin/bash -xe sudo yum -y install java-1.8.0 sudo yum -y remove java-1.7.0-openjdk</userdata>
            <numexecutors>10</numexecutors>
            <remoteadmin>ec2-user</remoteadmin>
            <subnetid>subnet-0a03c11b8ff3725e7</subnetid>
            <idleterminationminutes>30</idleterminationminutes>
            <iaminstanceprofile />
            <deleterootontermination>false</deleterootontermination>
            <useephemeraldevices>false</useephemeraldevices>
            <customdevicemapping />
            <instancecap>2147483647</instancecap>
            <stoponterminate>false</stoponterminate>
            <useprivatednsname>false</useprivatednsname>
            <associatepublicip>false</associatepublicip>
            <usededicatedtenancy>false</usededicatedtenancy>
            <amitype class="hudson.plugins.ec2.UnixData">
               <rootcommandprefix />
               <slavecommandprefix />
               <slavecommandsuffix />
               <sshport>22</sshport>
            </amitype>
            <launchtimeout>2147483647</launchtimeout>
            <connectbysshprocess>false</connectbysshprocess>
            <connectusingpublicip>false</connectusingpublicip>
            <nextsubnet>0</nextsubnet>
         </hudson.plugins.ec2.slavetemplate>
      </templates>
      <region>eu-central-1</region>
      <nodelayprovisioning>true</nodelayprovisioning>
   </hudson.plugins.ec2.ec2cloud>
</clouds>

Bild

Photo by Damien TUPINIER on Unsplash