Wir meistern die Aufgabe für die Bereitstellung in GKE ohne Plugins, SMS und Registrierung. Spähen Sie mit einem Auge unter Jenkins 'Jacke

Alles begann damit, dass ein Teamleiter eines unserer Entwicklungsteams im Testmodus darum bat, seine neue Anwendung, die am Tag zuvor in Containern enthalten war, offenzulegen. Ich habe es aufgestellt. Nach ca. 20 Minuten ging eine Anfrage zur Aktualisierung der Anwendung ein, da dort ein sehr notwendiges Stück fertiggestellt wurde. Ich erneuerte. Nach ein paar Stunden ... nun, Sie raten



bereits, was als nächstes geschah ... Ich muss zugeben, ich bin ziemlich faul (habe ich das früher zugegeben? Nein?) Und angesichts der Tatsache, dass Teamleiter Zugang zu Jenkins haben, in dem Wir haben alle CI / CD, dachte ich: Lassen Sie ihn sich so oft einsetzen, wie er will! Ich erinnerte mich an eine Anekdote: Gib einem Mann einen Fisch und er wird für den Tag voll sein; Nennen Sie eine Person gesättigt und er wird sein ganzes Leben lang gesättigt sein. Und er ging, um den Job zu spielenMein Großvater, ein Philologe, ein Englischlehrer in der Vergangenheit, würde jetzt seinen Finger an seiner Schläfe drehen und mich nach dem Lesen sehr ausdrucksstark ansehen , um einen Container in einem Kuber unter Anwendung einer erfolgreich zusammengestellten Version einzusetzen und alle ENV- Werte darauf zu übertragen Satz).



In einem Beitrag werde ich darüber sprechen, wie ich gelernt habe:



  1. Aktualisieren Sie Jobs in Jenkins dynamisch vom Job selbst oder von anderen Jobs.
  2. Stellen Sie vom Knoten aus mit dem installierten Jenkins-Agenten eine Verbindung zur Cloud-Konsole (Cloud-Shell) her.
  3. Stellen Sie eine Workload für die Google Kubernetes Engine bereit.


Tatsächlich bin ich natürlich ein bisschen gerissen. Es wird davon ausgegangen, dass sich mindestens ein Teil Ihrer Infrastruktur in der Google Cloud befindet. Daher sind Sie deren Benutzer und haben natürlich ein GCP-Konto. Aber in der Notiz geht es nicht darum.



Dies ist mein nächster Spickzettel. Ich möchte solche Notizen nur in einem Fall schreiben: Ich hatte ein Problem vor mir, ich wusste zunächst nicht, wie ich es lösen sollte, die Lösung wurde nicht in ihrer fertigen Form gegoogelt, also habe ich sie in Teilen gegoogelt und schließlich das Problem gelöst. Und damit ich in Zukunft, wenn ich vergesse, wie ich es gemacht habe, nicht alles Stück für Stück neu googeln und zusammenstellen muss, schreibe ich mir solche Spickzettel.

Disclaimer: 1. « », best practice . « » .

2. , , , — .

Jenkins



Ich sehe Ihre Frage voraus: Was hat die dynamische Jobaktualisierung damit zu tun? Ich habe den Wert des String-Parameters mit den Handles eingegeben und gehe weiter!



Die Antwort lautet: Ich bin wirklich faul, ich mag es nicht, wenn sich Leute beschweren: Mischa, der Einsatz stürzt ab, alles ist weg! Sie beginnen zu suchen, und der Wert eines Task-Startparameters enthält einen Tippfehler. Deshalb mache ich lieber alles so komplett wie möglich. Wenn es möglich ist, den Benutzer daran zu hindern, Daten direkt einzugeben, indem stattdessen eine Liste mit Werten zur Auswahl angegeben wird, organisiere ich die Auswahl.



Der Plan lautet wie folgt: Erstellen Sie einen Job in Jenkins, in dem Sie vor dem Start eine Version aus der Liste auswählen, Werte für die über ENV an den Container übergebenen Parameter angeben, den Container sammeln und an die Containerregistrierung senden können. Weiter von dort wird der Container in kubera as gestartetArbeitslast mit den im Job angegebenen Parametern.



Wir werden den Prozess des Erstellens und Konfigurierens eines Jobs in Jenkins nicht berücksichtigen, dies ist offtopisch. Wir gehen davon aus, dass die Aufgabe fertig ist. Um eine aktualisierbare Versionsliste zu implementieren, benötigen wir zwei Dinge: eine vorhandene Quellliste mit a priori gültigen Versionsnummern und eine Variable vom Typ Choice-Parameter in der Aufgabe. In unserem Beispiel soll die Variable BUILD_VERSION heißen , wir werden nicht im Detail darauf eingehen. Aber schauen wir uns die Quellenliste genauer an.



Es gibt nicht so viele Möglichkeiten. Zwei fielen mir sofort ein:



  • Verwenden Sie die RAS-API, die Jenkins seinen Benutzern anbietet.
  • Fragen Sie den Inhalt des Remote-Repository-Ordners ab (in unserem Fall ist dies JFrog Artifactory, was nicht wichtig ist).


Jenkins RAS-API



Nach der etablierten feinen Tradition vermeide ich lieber lange Erklärungen.

Ich werde mir nur erlauben, einen Teil des ersten Absatzes der ersten Seite der API-Dokumentation frei zu übersetzen :

Jenkins bietet eine API für den maschinenlesbaren Remotezugriff auf seine Funktionen. <...> Der Fernzugriff wird in einem REST-ähnlichen Stil angeboten. Dies bedeutet, dass es keinen einzigen Einstiegspunkt für alle Funktionen gibt. Stattdessen wird eine URL wie " ... / api / " verwendet, wobei " ... " das Objekt ist, auf das die API-Funktionen angewendet werden.
Mit anderen Worten, wenn die Bereitstellungsaufgabe, über die wir gerade sprechen, unter der Adresse verfügbar ist http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, sind die API-Pfeifen für diese Aufgabe unter Weiter verfügbar . Wir haben die Wahl, in welcher Form die Ausgabe empfangen werden soll. Lassen Sie uns auf XML eingehen, da die API nur in diesem Fall das Filtern zulässt. Versuchen wir einfach, eine Liste aller Jobläufe zu erhalten. Wir interessieren uns nur für den Assemblynamen ( displayName ) und dessen Ergebnis ( result ):http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/











http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]


Passierte?



Filtern wir nun nur die Starts heraus, die zu einem ERFOLGS- Ergebnis führen . Wir verwenden das Argument & exclude und übergeben ihm den Pfad zu einem Wert, der nicht gleich SUCCESS ist, als Parameter . Ja Ja. Doppelte Negation ist eine Aussage. Wir schließen alles aus, was uns nicht interessiert:



http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS']


Screenshot der Liste der erfolgreichen




Lassen Sie uns nur zum Verwöhnen sicherstellen, dass der Filter uns nicht täuscht (Filter lügen nie!) Und eine Liste der „erfolglosen“ Filter anzeigen:



http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS']


Screenshot der Liste der erfolglosen




Liste der Versionen aus einem Ordner auf einem Remote-Server



Es gibt auch eine zweite Möglichkeit, eine Liste der Versionen zu erhalten. Ich mag es noch mehr als den Jenkins API-Aufruf. Nun, wenn die Anwendung erfolgreich erstellt wurde, wurde sie gepackt und im entsprechenden Ordner im Repository abgelegt. Ein Repository ist standardmäßig ein Repository mit Arbeitsversionen von Anwendungen. Mögen. Fragen wir ihn, welche Versionen gespeichert sind. Wir werden den Remote-Ordner kräuseln, grep und awk. Wenn sich jemand für den Unliner interessiert, dann ist er unter dem Spoiler.



Einzeiliger Befehl
: , , . :



curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)\|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>\K[^/]+' )




Job-Setup und Job-Konfigurationsdatei in Jenkins



Wir haben uns mit der Quelle der Versionsliste befasst. Lassen Sie uns nun die resultierende Liste in die Aufgabe einschrauben. Für mich bestand die offensichtliche Lösung darin, dem Anwendungserstellungsjob einen Schritt hinzuzufügen. Der Schritt, der ausgeführt würde, wenn das Ergebnis "Erfolg" ist.



Öffnen Sie die Einstellungen für die Montageaufgabe und scrollen Sie ganz nach unten. Klicken Sie auf die Schaltflächen: Build-Schritt hinzufügen -> Bedingter Schritt (einzeln) . Wählen Sie in den Schritteinstellungen die Bedingung Aktueller Build-Status aus , legen Sie den SUCCESS- Wert fest , die Aktion, die ausgeführt werden soll, wenn der Befehl Shell ausführen erfolgreich ist .



Und jetzt der lustige Teil. Jenkins speichert Jobkonfigurationen in Dateien. Im XML-Format. Auf dem Weghttp://--/config.xmlDementsprechend können Sie die Konfigurationsdatei herunterladen, nach Bedarf bearbeiten und an der Stelle ablegen, an der sie entnommen wurde.



Denken Sie daran, dass wir oben vereinbart haben, einen BUILD_VERSION- Parameter für die Versionsliste zu erstellen .



Laden Sie die Konfigurationsdatei herunter und werfen Sie einen Blick hinein. Nur um sicherzustellen, dass der Parameter vorhanden und wirklich die richtige Art ist.



Screenshot unter dem Spoiler.



Ihr config.xml-Snippet sollte gleich aussehen. Nur dass der Inhalt des Auswahlelements noch nicht vorhanden ist




Bist du überzeugt In Ordnung, wir schreiben ein Skript, das im Falle eines erfolgreichen Builds ausgeführt wird.

Das Skript erhält eine Liste der Versionen, lädt eine Konfigurationsdatei herunter, schreibt eine Liste der Versionen an der von uns benötigten Stelle hinein und legt sie dann wieder ab. Ja. Alles ist richtig. Schreiben Sie eine Liste der Versionen in XML an die Stelle, an der bereits eine Liste der Versionen vorhanden ist (wird in Zukunft nach dem ersten Start des Skripts verfügbar sein). Ich weiß, dass es auf der Welt immer noch einige wilde Liebhaber regulärer Ausdrücke gibt. Ich gehöre nicht zu ihnen. Bitte installieren Sie xmlstarler auf dem Computer, auf dem die Konfiguration bearbeitet wird. Es scheint mir, dass dies kein großer Preis ist, um die XML-Bearbeitung mit sed zu vermeiden.



Unter dem Spoiler zitiere ich den Code, der die gesamte oben beschriebene Sequenz ausführt.



Wir schreiben die Liste der Versionen aus dem Ordner auf dem Remote-Server in die Konfiguration
#!/bin/bash
##############  
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

##############     xml-   
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

##############       
readarray -t vers < <( curl -H "X-JFrog-Art-Api:Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)\|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>\K[^/]+' )

##############       
printf '%s\n' "${vers[@]}" | sort -r | \
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

##############   
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

##############     
rm -f appConfig.xml




Wenn Ihnen die Möglichkeit gefallen hat, Versionen von Jenkins mehr zu erhalten, und Sie so faul sind wie ich, dann haben Sie unter dem Spoiler denselben Code, aber die Liste stammt von Jenkins:



Wir schreiben eine Liste von Versionen von Jenkins in die Konfiguration
: , . , awk . .



#!/bin/bash
##############  
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

##############     xml-   
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

##############       Jenkins
curl -g -X GET -u username:apiKey 'http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!=%22SUCCESS%22]&pretty=true' -o builds.xml

##############       XML
readarray vers < <(xmlstarlet sel -t -v "freeStyleProject/allBuild/displayName" builds.xml | awk -F":" '{print $2}')

##############       
printf '%s\n' "${vers[@]}" | sort -r | \
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

##############   
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

##############     
rm -f appConfig.xml




Wenn Sie den auf der Grundlage der obigen Beispiele geschriebenen Code getestet haben, sollten Sie theoretisch in der Bereitstellungsaufgabe bereits eine Dropdown-Liste mit Versionen haben. Hier ist so etwas wie der Screenshot unter dem Spoiler.



Richtig ausgefüllte Versionsliste




Wenn alles funktioniert hat, kopieren Sie das Skript, fügen Sie es in den Befehl Shell ausführen ein und speichern Sie die Änderungen.



Cloud-Shell-Verbindung



Sammler sind in unseren Containern. Wir verwenden Ansible als Anwendungsbereitstellungs- und Konfigurationsmanager. Dementsprechend fallen beim Erstellen von Containern drei Optionen ein: Installieren Sie Docker in Docker, installieren Sie Docker auf einem Computer mit Ansible oder erstellen Sie Container in der Cloud-Konsole. Wir haben vereinbart, in diesem Artikel über Plugins für Jenkins zu schweigen. Merken?



Ich entschied: Nun, da die Container "out of the box" in der Cloud-Konsole zusammengebaut werden können, warum dann einen Gemüsegarten umzäunen? Halte es sauber, oder? Ich möchte Container mit Jenkins in der Cloud-Konsole sammeln und sie dann von dort aus in kuber schießen. Darüber hinaus verfügt Google über sehr umfangreiche Kanäle in der Infrastruktur, was sich positiv auf die Bereitstellungsgeschwindigkeit auswirkt.



Für die Verbindung zur Cloud-Konsole sind zwei Dinge erforderlich : gcloudund Zugriffsrechte auf die Google Cloud-API für die VM-Instanz, von der aus diese Verbindung hergestellt wird.



Für diejenigen, die keine Verbindung über die Google Cloud herstellen möchten
. , *nix' .



, — . — .



Der einfachste Weg, Berechtigungen zu erteilen, ist über die Weboberfläche.



  1. Stoppen Sie die VM-Instanz, von der aus Sie in Zukunft eine Verbindung zur Cloud-Konsole herstellen werden.
  2. Öffnen Sie die Instanzdetails und klicken Sie auf Bearbeiten .
  3. Wählen Sie ganz unten auf der Seite den Instanzzugriffsbereich aus. Voller Zugriff auf alle Cloud-APIs .



    Bildschirmfoto


  4. Speichern Sie Ihre Änderungen und starten Sie die Instanz.


Stellen Sie nach dem Booten der VM eine Verbindung über SSH her und stellen Sie sicher, dass die Verbindung erfolgreich ist. Verwenden Sie den Befehl:



gcloud alpha cloud-shell ssh


Eine erfolgreiche Verbindung sieht ungefähr so ​​aus




Bereitstellen auf GKE



Da wir uns auf jede erdenkliche Weise bemühen, vollständig auf IaC (Infrastucture as a Code) umzusteigen, speichern wir Docker-Dateien in der Gita. Dies ist einerseits. Eine Bereitstellung in Kubernetes wird durch eine Yaml-Datei beschrieben, die nur von dieser Aufgabe verwendet wird, die selbst ebenfalls einem Code ähnelt. Das ist auf der anderen Seite. Im Allgemeinen meine ich, dass der Plan wie folgt lautet:



  1. Wir nehmen die Werte der Variablen BUILD_VERSION und optional die Werte der Variablen, die durch ENV übergeben werden .
  2. Dockerfile von der Gita herunterladen.
  3. Yaml für die Bereitstellung generieren.
  4. Laden Sie diese beiden Dateien über scp in die Cloud-Konsole hoch.
  5. Erstellen Sie dort einen Container und verschieben Sie ihn in die Container-Registrierung
  6. Wir wenden die Lastbereitstellungsdatei auf den Kuber an.


Lassen Sie uns genauer sein. Da es sich um ENV handelt , müssen wir die Werte von zwei Parametern übergeben: PARAM1 und PARAM2 . Fügen Sie ihre Aufgabe für die Bereitstellung hinzu und geben Sie - String Parameter ein .



Bildschirmfoto




Wir werden yaml generieren, indem wir einfach das Echo in eine Datei umleiten . Es wird natürlich davon ausgegangen, dass Sie PARAM1 und PARAM2 in der Docker-Datei haben , dass der Name der Ladung awesomeapp lautet und der zusammengestellte Container mit der Anwendung der angegebenen Version sich in der Container-Registrierung entlang des Pfads gcr.io/awesomeapp/awesomeapp- $ BUILD_VERSION befindet , wobei $ BUILD_VERSION nur ist wurde aus der Dropdown-Liste ausgewählt.



Befehle auflisten
touch deploy.yaml
echo "apiVersion: apps/v1" >> deploy.yaml
echo "kind: Deployment" >> deploy.yaml
echo "metadata:" >> deploy.yaml
echo "  name: awesomeapp" >> deploy.yaml
echo "spec:" >> deploy.yaml
echo "  replicas: 1" >> deploy.yaml
echo "  selector:" >> deploy.yaml
echo "    matchLabels:" >> deploy.yaml
echo "      run: awesomeapp" >> deploy.yaml
echo "  template:" >> deploy.yaml
echo "    metadata:" >> deploy.yaml
echo "      labels:" >> deploy.yaml
echo "        run: awesomeapp" >> deploy.yaml
echo "    spec:" >> deploy.yaml
echo "      containers:" >> deploy.yaml
echo "      - name: awesomeapp" >> deploy.yaml
echo "        image: gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION:latest" >> deploy.yaml
echo "        env:" >> deploy.yaml
echo "        - name: PARAM1" >> deploy.yaml
echo "          value: $PARAM1" >> deploy.yaml
echo "        - name: PARAM2" >> deploy.yaml
echo "          value: $PARAM2" >> deploy.yaml




Nach dem Herstellen einer Verbindung mit gcloud alpha cloud-shell ssh zum Jenkins-Agenten ist der interaktive Modus nicht verfügbar. Daher senden wir Befehle mit dem Parameter --command an die Cloud-Konsole .



Wir bereinigen den Home-Ordner in der Cloud-Konsole von der alten Docker-Datei:



gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"


Wir legen die frisch heruntergeladene Docker-Datei mit scp in den Home-Ordner der Cloud-Konsole:



gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~


Wir sammeln, markieren und schieben den Container in die Container-Registrierung:



gcloud alpha cloud-shell ssh --command="docker build -t awesomeapp-$BUILD_VERSION ./ --build-arg BUILD_VERSION=$BUILD_VERSION --no-cache"
gcloud alpha cloud-shell ssh --command="docker tag awesomeapp-$BUILD_VERSION gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"
gcloud alpha cloud-shell ssh --command="docker push gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"


Dasselbe machen wir mit der Bereitstellungsdatei. Beachten Sie, dass die folgenden Befehle fiktive Namen für den Cluster verwenden, in dem die Bereitstellung stattfindet ( awsm-cluster ), und den Namen des Projekts ( awesome-project ), in dem sich der Cluster befindet.



gcloud alpha cloud-shell ssh --command="rm -f deploy.yaml"
gcloud alpha cloud-shell scp localhost:./deploy.yaml cloudshell:~
gcloud alpha cloud-shell ssh --command="gcloud container clusters get-credentials awsm-cluster --zone us-central1-c --project awesome-project && \
kubectl apply -f deploy.yaml"


Wir starten die Aufgabe, öffnen die Konsolenausgabe und hoffen auf einen erfolgreichen Containeraufbau.



Bildschirmfoto




Und dann der erfolgreiche Einsatz des zusammengebauten Containers



Bildschirmfoto




Ich habe das Ingress- Setup absichtlich ignoriert . Aus einem einfachen Grund: Sobald es für eine Workload mit einem bestimmten Namen konfiguriert wurde , bleibt es betriebsbereit, unabhängig davon, wie viele Bereitstellungen mit diesem Namen ausgeführt werden. Nun, im Allgemeinen geht dies etwas über den Rahmen der Geschichte hinaus.



Anstelle von Schlussfolgerungen



Alle oben genannten Schritte konnten wahrscheinlich nicht ausgeführt werden, sondern installierten einfach ein Plugin für Jenkins, ihr Muuulon. Aber irgendwie mag ich keine Plugins. Genauer gesagt greife ich nur aus Verzweiflung auf sie zurück.



Und ich greife einfach gerne ein neues Thema für mich auf. Der obige Text ist auch eine Möglichkeit, die von mir gemachten Erkenntnisse zu teilen und das am Anfang beschriebene Problem zu lösen. Teilen Sie mit denen, die in Devops überhaupt kein schrecklicher Wolf sind. Wenn meine Erkenntnisse zumindest jemandem helfen, werde ich glücklich sein.



All Articles