Nous maîtrisons la tâche de déploiement dans GKE sans plugins, SMS et enregistrement. Coup d'oeil sous la veste de Jenkins avec un œil

Tout a commencé avec le fait qu'un chef d'équipe de l'une de nos équipes de développement a demandé en mode test d'exposer leur nouvelle application, qui avait été conteneurisée la veille. Je l'ai mis en place. Après environ 20 minutes, une demande a été reçue pour mettre à jour l'application, car une pièce très nécessaire y était terminée. J'ai renouvelé. Après quelques heures supplémentaires ... eh bien, vous devinez



déjà ce qui a commencé à se passer ensuite ... Je dois admettre que je suis plutôt paresseux (ai-je admis cela plus tôt? Non?), Et, étant donné que les chefs d'équipe ont accès à Jenkins, dans lequel Nous avons tous les CI / CD, je me suis dit: laissez-le se déployer à sa guise! Je me suis souvenu d'une blague: donnez un poisson à un homme et il sera rassasié pour la journée; appeler une personne Sated et il sera Sated toute sa vie. Et il est allé jouer avec le travail, qui serait capable de déployer un conteneur dans un Kuber avec l'application de n'importe quelle version assemblée avec succès et de lui transférer toutes les valeurs ENV (mon grand-père, un philologue, un professeur d'anglais dans le passé, tordait maintenant son doigt sur sa tempe et me regardait de manière très expressive après avoir lu ceci phrase).



Donc, dans un article, je parlerai de comment j'ai appris:



  1. Mettre à jour dynamiquement les travaux dans Jenkins à partir du travail lui-même ou à partir d'autres travaux;
  2. Connectez-vous à la console cloud (Cloud shell) à partir du nœud avec l'agent Jenkins installé;
  3. Déployez une charge de travail sur Google Kubernetes Engine.


En fait, je suis, bien sûr, un peu rusé. On suppose qu'au moins une partie de votre infrastructure se trouve dans le cloud Google, et que vous en êtes donc l'utilisateur et, bien sûr, vous disposez d'un compte GCP. Mais la note n'est pas à ce sujet.



Ceci est ma prochaine feuille de triche. Je ne veux écrire de telles notes que dans un cas: j'avais un problème devant moi, je ne savais pas au départ comment le résoudre, la solution n'était pas googlé dans sa forme finie, alors je l'ai googlé par parties et j'ai finalement résolu le problème. Et pour qu'à l'avenir, quand j'oublie comment je l'ai fait, je n'ai plus à tout chercher sur Google morceau par morceau et à le compiler ensemble, je m'écris de telles feuilles de triche.

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

2. , , , — .

Jenkins



Je prévois votre question: qu'est-ce que la mise à jour dynamique des emplois a à voir avec cela? J'ai entré la valeur du paramètre de chaîne avec les poignées et continuez!



La réponse est: je suis vraiment paresseux, je n'aime pas quand ils se plaignent: Misha, le déploiement plante, tout est parti! Vous commencez à chercher, et il y a une faute de frappe dans la valeur d'un paramètre de lancement de tâche. Par conséquent, je préfère tout faire le plus complètement possible. S'il est possible d'empêcher l'utilisateur de saisir directement des données en donnant une liste de valeurs à sélectionner à la place, j'organise la sélection.



Le plan est le suivant: créer un job dans Jenkins, dans lequel, avant le lancement, il serait possible de sélectionner une version dans la liste, de spécifier les valeurs des paramètres passés au conteneur via ENV , puis il collecte le conteneur et le pousse vers le Container Registry. Plus loin de là, le conteneur est lancé à kubera commecharge de travail avec les paramètres spécifiés dans le travail.



Nous ne considérerons pas le processus de création et de configuration d'un travail dans Jenkins, c'est hors sujet. Nous supposerons que la tâche est prête. Pour implémenter une liste de versions actualisable, nous avons besoin de deux choses: une liste source existante avec des numéros de version a priori valides et une variable du type de paramètre Choice dans la tâche. Dans notre exemple, que la variable s'appelle BUILD_VERSION , nous ne nous attarderons pas dessus en détail. Mais regardons de plus près la liste des sources.



Il n'y a pas tellement d'options. Deux me sont immédiatement venues à l'esprit:



  • Utilisez l'API d'accès à distance que Jenkins propose à ses utilisateurs;
  • Interrogez le contenu du dossier du référentiel distant (dans notre cas, il s'agit de JFrog Artifactory, ce qui n'est pas important).


API d'accès à distance Jenkins



Selon la belle tradition établie, je préfère éviter les longues explications.

Je ne m'autoriserai qu'à traduire librement un morceau du premier paragraphe de la première page de la documentation de l'API :

Jenkins fournit une API pour un accès lisible par machine à distance à ses fonctionnalités. <...> L'accès à distance est proposé dans un style de type REST. Cela signifie qu'il n'y a pas de point d'entrée unique pour toutes les capacités, et à la place une URL comme " ... / api / " est utilisée, où " ... " est l'objet auquel les capacités de l'API sont appliquées.
En d'autres termes, si la tâche de déploiement, dont nous parlons pour le moment, est disponible à l'adresse http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, alors les sifflets API pour cette tâche sont disponibles à Next, nous avons le choix sous quelle forme recevoir la sortie. Attardons-nous sur XML, puisque l'API n'autorise que le filtrage dans ce cas. Essayons simplement d'obtenir une liste de tous les travaux exécutés. Nous ne sommes intéressés que par le nom de l'assembly ( displayName ) et son résultat ( 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]


Arrivé?



Maintenant, filtrons uniquement les lancements qui aboutissent à un résultat SUCCESS . Nous utilisons l'argument & exclude et lui passons le chemin vers une valeur différente de SUCCESS en tant que paramètre . Oui oui. La double négation est une affirmation. Nous excluons tout ce qui ne nous intéresse pas:



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


Capture d'écran de la liste des




Eh bien, juste pour se faire dorloter, assurons-nous que le filtre ne nous a pas trompés (les filtres ne mentent jamais!) Et affichons une liste de ceux qui ont échoué:



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


Capture d'écran de la liste des échecs




Liste des versions d'un dossier sur un serveur distant



Il existe également un deuxième moyen d'obtenir une liste de versions. Je l'aime encore plus que l'appel à l'API Jenkins. Eh bien, parce que si l'application a été créée avec succès, elle a été empaquetée et placée dans le référentiel dans le dossier approprié. Comme, un référentiel est par défaut un référentiel de versions fonctionnelles des applications. Comme. Eh bien, demandons-lui quelles versions sont stockées. Nous allons curl, grep et awk le dossier distant. Si quelqu'un s'intéresse à la doublure, c'est sous le spoiler.



Commande en une ligne
: , , . :



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[^/]+' )




Configuration du travail et fichier de configuration du travail dans Jenkins



Nous avons traité de la source de la liste des versions. Visionnons maintenant la liste résultante dans la tâche. Pour moi, la solution évidente était d'ajouter une étape dans le travail de création de l'application. L'étape qui serait effectuée si le résultat était "succès".



Ouvrez les paramètres de la tâche d'assemblage et faites défiler vers le bas. Cliquez sur les boutons: Ajouter une étape de construction -> Étape conditionnelle (unique) . Dans les paramètres de l'étape, sélectionnez la condition État actuel de construction , définissez la valeur SUCCESS , l'action à effectuer si la commande Exécuter le shell réussit .



Et maintenant la partie amusante. Jenkins stocke les configurations de travail dans des fichiers. Au format XML. Le long du cheminhttp://--/config.xmlEn conséquence, vous pouvez télécharger le fichier de configuration, le modifier si nécessaire et le mettre à l'endroit d'où il a été extrait.



Rappelez-vous, ci-dessus, nous avons convenu de créer un paramètre BUILD_VERSION pour la liste des versions ?



Téléchargeons le fichier de configuration et jetons un œil à l'intérieur. Juste pour s'assurer que le paramètre est en place et vraiment le bon type.



Capture d'écran sous le spoiler.



Votre extrait de code config.xml doit avoir la même apparence. Sauf que le contenu de l'élément choice n'est pas encore présent




Êtes-vous convaincu? Très bien, nous écrivons un script qui sera exécuté en cas de construction réussie.

Le script recevra une liste de versions, téléchargera un fichier de configuration, y écrira une liste de versions à l'endroit dont nous avons besoin, puis le remettra. Oui. Tout est correct. Ecrivez une liste de versions en XML à l'endroit où il y a déjà une liste de versions (sera dans le futur, après le premier lancement du script). Je sais qu'il y a encore de féroces amateurs d'expressions régulières dans le monde. Je ne leur appartiens pas. Veuillez installer xmlstarler sur la machine où la configuration sera modifiée. Il me semble que ce n'est pas un gros prix à payer pour éviter l'édition XML avec sed.



Sous le spoiler, je cite le code qui exécute toute la séquence décrite ci-dessus.



Nous écrivons dans la config la liste des versions du dossier sur le serveur distant
#!/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




Si vous avez plus aimé la possibilité d'obtenir des versions de Jenkins et que vous êtes aussi paresseux que moi, alors sous le spoiler, le même code, mais la liste vient de Jenkins:



Nous écrivons une liste de versions de Jenkins à la configuration
: , . , 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




En théorie, si vous avez testé le code écrit sur la base des exemples ci-dessus, dans la tâche de déploiement, vous devriez déjà avoir une liste déroulante avec les versions. Voici quelque chose comme la capture d'écran sous le spoiler.



Liste des versions correctement complétée




Si tout a fonctionné, copiez et collez le script dans la commande Exécuter le shell et enregistrez les modifications.



Connexion Cloud Shell



Les collectionneurs sont dans nos conteneurs. Nous utilisons Ansible comme notre gestionnaire de configuration et de livraison d'applications. En conséquence, lorsqu'il s'agit de créer des conteneurs, trois options viennent à l'esprit: installer Docker dans Docker, installer Docker sur une machine avec Ansible ou créer des conteneurs dans la console cloud. Nous avons accepté de ne pas parler des plugins pour Jenkins dans cet article. Rappelles toi?



J'ai décidé: eh bien, puisque les conteneurs «prêts à l'emploi» peuvent être assemblés dans la console cloud, alors pourquoi clôturer le jardin? Gardez-le propre, non? Je souhaite créer des conteneurs avec Jenkins dans la console cloud, puis les envoyer dans Kuber à partir de là. De plus, à l'intérieur de l'infrastructure, Google a tellement de gros canaux, ce qui aura un effet bénéfique sur la vitesse de déploiement.



Deux choses sont nécessaires pour se connecter à la console cloud: gcloudet les droits d'accès à l' API Google Cloud pour l'instance de VM à partir de laquelle cette connexion sera établie.



Pour ceux qui prévoient de ne pas se connecter du tout depuis le cloud Google
. , *nix' .



, — . — .



Le moyen le plus simple d'accorder des autorisations consiste à utiliser l'interface Web.



  1. Arrêtez l'instance de VM à partir de laquelle vous vous connecterez à la console cloud à l'avenir.
  2. Ouvrez les détails de l'instance et cliquez sur Modifier .
  3. Tout en bas de la page, sélectionnez la portée de l' accès à l'instance Accès complet à toutes les API Cloud .



    Capture d'écran


  4. Enregistrez vos modifications et démarrez l'instance.


Une fois le démarrage de la VM terminé, connectez-vous-y via SSH et assurez-vous que la connexion est réussie. Utilisez la commande:



gcloud alpha cloud-shell ssh


Une connexion réussie ressemble à ceci




Déployer sur GKE



Puisque nous nous efforçons de toutes les manières possibles de passer complètement à IaC (Infrastucture as a Code), nous stockons les fichiers dockerfiles dans la gita. C'est d'une part. Un déploiement dans kubernetes est décrit par un fichier yaml qui n'est utilisé que par cette tâche, qui lui-même est également comme un code. C'est de l'autre côté. En général, je veux dire que le plan est le suivant:



  1. Nous prenons les valeurs des variables BUILD_VERSION et, éventuellement, les valeurs des variables qui seront passées par ENV .
  2. Téléchargement de dockerfile depuis le gita.
  3. Génération de yaml pour le déploiement.
  4. Téléchargez ces deux fichiers via scp sur la console cloud.
  5. Créez un conteneur là-bas et poussez-le dans le registre des conteneurs
  6. Nous appliquons le fichier de déploiement de charge au Kuber.


Soyons plus précis. Puisque nous parlons d' ENV , disons que nous devons passer les valeurs de deux paramètres: PARAM1 et PARAM2 . Ajoutez leur tâche pour le déploiement, tapez - Paramètre de chaîne .



Capture d'écran




Nous allons générer yaml en redirigeant simplement echo vers un fichier. Il est supposé, bien sûr, que vous avez PARAM1 et PARAM2 dans le dockerfile , que le nom de chargement sera awesomeapp et que le conteneur assemblé avec l'application de la version spécifiée se trouve dans le registre des conteneurs le long du chemin gcr.io/awesomeapp/awesomeapp- $ BUILD_VERSION , où $ BUILD_VERSION est juste a été sélectionné dans la liste déroulante.



Liste des commandes
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




Après la connexion à l'aide de gcloud alpha cloud-shell ssh à l' agent Jenkins, le mode interactif n'est pas disponible, nous envoyons donc des commandes à la console cloud à l'aide du paramètre --command .



Nous nettoyons le dossier d'accueil dans la console cloud de l'ancien fichier docker:



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


Nous mettons le dockerfile fraîchement téléchargé dans le dossier d'accueil de la console cloud à l'aide de scp:



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


Nous collectons, étiquetons et poussons le conteneur vers le registre des conteneurs:



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"


Nous faisons de même avec le fichier de déploiement. Notez que les commandes ci-dessous utilisent des noms fictifs pour le cluster où le déploiement a lieu ( awsm-cluster ) et le nom du projet ( awesome-project ) où se trouve le cluster.



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"


Nous commençons la tâche, ouvrons la sortie de la console et espérons voir la construction du conteneur avec succès.



Capture d'écran




Et puis le déploiement réussi du conteneur assemblé



Capture d'écran




J'ai délibérément ignoré la configuration d' Ingress . Pour une raison simple: une fois configuré pour une charge de travail avec un nom donné, il restera opérationnel, quel que soit le nombre de déploiements avec ce nom. Eh bien, en général, cela dépasse un peu le cadre de l'histoire.



Au lieu de conclusions



Toutes les étapes ci-dessus, probablement, n'auraient pas pu être effectuées, mais ont simplement installé un plugin pour Jenkins, leur muuulion. Mais d'une manière ou d'une autre, je n'aime pas les plugins. Eh bien, plus précisément, je n'y recourt que par désespoir.



Et j'aime juste aborder un nouveau sujet pour moi. Le texte ci-dessus est également un moyen de partager les conclusions que j'ai faites, en résolvant le problème décrit au tout début. Partagez avec ceux qui, comme, ne sont pas du tout un loup terrible dans les devops. Si mes découvertes aident au moins quelqu'un, je serai heureux.



All Articles