Wszystko zaczęło się od tego, że lider jednej z naszych drużyn deweloperskich poprosił o wystawienie ich nowej aplikacji, która dzień wcześniej przeszła konteneryzację, w trybie testowym. Wystawiłem. Około 20 minut później przyszła prośba o aktualizację aplikacji, ponieważ dodano w niej bardzo potrzebną funkcję. Zaktualizowałem. Jeszcze po kilku godzinach… no, możecie się domyślić, co się działo dalej…
Muszę przyznać, że jestem dość leniwy (przyznawałem się do tego wcześniej? nie?), i biorąc pod uwagę, że liderzy mają dostęp do Jenkinsa, w którym mamy całe CI/CD, pomyślałem: niech on sam wdraża, ile mu się podoba! Przypomniałem sobie dowcip: daj człowiekowi rybę, a będzie najedzony przez dzień; nazwij człowieka Najedzony, a będzie najedzony przez całe życie. I poszedłem zmajstrować zadanie, które umie wdrażać kontener z aplikacją dowolnej pomyślnie zbudowanej wersji i przekazywać mu dowolne wartości ENV (mój dziadek, filolog, nauczyciel angielskiego w przeszłości, teraz pewnie kręciłby palcem przy skroni i bardzo wyraźnie spojrzałby na mnie, czytając to zdanie).
Tak więc, w notatce opowiem o tym, jak się nauczyłem:
- Dynamicznie aktualizować zadania w Jenkinsie z samego zadania lub z innych zadań;
- Łączyć się z konsolą chmurową (Cloud Shell) z węzła z zainstalowanym agentem Jenkinsa;
- Wdrażać obciążenia (workload) w Google Kubernetes Engine.
W rzeczywistości, oczywiście, trochę oszukuję. Zakłada się, że przynajmniej część infrastruktury jest w chmurze Google, a zatem jesteś użytkownikiem, i oczywiście masz konto GCP. Ale notatka nie jest o tym.
To kolejna moja ściągawka. Takie notatki chcę pisać tylko w jednym przypadku: stało przede mną zadanie, początkowo nie wiedziałem, jak je rozwiązać, rozwiązanie nie znalazło się w gotowej formie, więc szukałem go po częściach i ostatecznie je rozwiązałem. A żeby w przyszłości, gdy zapomnę, jak to zrobiłem, nie musieć na nowo wszystkiego guglować po kawałkach i kompilować w całość, piszę sobie takie ściągawki.
Informacja: 1. Notatka była pisana „dla siebie”, nie aspiruje do miana najlepszej praktyki. Z przyjemnością przeczytam w komentarzach propozycje „a lepiej byłoby zrobić tak”.
2. Jeśli część praktyczna notatki traktować jako sól, to, jak wszystkie moje poprzednie notatki, ta jest słabo solonym roztworem.
Dynamiczne aktualizowanie ustawień zadań w Jenkinsie
Widzę już Twoje pytanie: co ma wspólnego dynamiczna aktualizacja zadania? Wpisałem ręcznie wartość parametru tekstowego i do przodu!
Odpowiadam: faktycznie jestem leniwy, nie lubię, gdy ludzie narzekają: Misza, wdrożenie się psuje, wszystko zginęło! Zaczynasz sprawdzać, a tam literówka w wartości jakiegoś parametru uruchamiającego zadanie. Dlatego wolę wszystko robić możliwie przebiegle. Jeśli istnieje możliwość odebrania użytkownikowi możliwości wprowadzania danych bezpośrednio, zamiast tego dając mu listę wartości do wyboru, to organizuję wybór.
Plan jest taki: tworzymy zadanie w Jenkinsie, w którym przed uruchomieniem można byłoby z listy wybrać wersję, wskazać wartości dla parametrów, przekazywanych do kontenera przez ENV, następnie buduje kontener i wypycha go do Container Registry. Stamtąd kontener uruchamiany jest w Kubernetach jako workload z parametrami określonymi w zadaniu.
Nie będziemy rozważać procesu tworzenia i konfiguracji zadania w Jenkinsie, to off-topic. Zakładamy, że zadanie jest gotowe. Aby zrealizować aktualizowaną listę wersji, potrzebujemy dwóch rzeczy: już istniejącej listy źródłowej z z góry walidowanymi numerami wersji oraz zmiennej typu Choice parameter w zadaniu. W naszym przykładzie niech zmienna nosi nazwę BUILD_VERSION, na tym nie będziemy się szczegółowo zatrzymywać. Ale na liście źródłowej zatrzymajmy się nieco dłużej.
Opcji nie jest zbyt wiele. Od razu przyszły mi na myśl dwie:
- Użyć Remote access API, które Jenkins oferuje swoim użytkownikom;
- Pobierać zawartość zdalnego folderu repozytorium (w naszym przypadku jest to JFrog Artifactory, co nie ma znaczenia).
Jenkins Remote access API
Zgodnie z piękną tradycją wolę unikać rozwlekłych wyjaśnień.
Pozwolę sobie tylko na swobodny przekład fragmentu pierwszego akapitu :
Jenkins oferuje API dla zdalnego, maszynowo zrozumiałego dostępu do swoich funkcjonalności. Zdalny dostęp oferowany jest w stylu podobnym do REST. Oznacza to, że nie istnieje jednolity punkt wejścia do wszystkich możliwości, a zamiast tego używa się URL w formacie "…/api/", gdzie "…" oznacza obiekt, do którego stosują się możliwości API.
Innymi słowy, jeśli zadanie na wdrożenie, o którym mówimy, jest dostępne pod adresem http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, to API-funkcje do tego zadania są dostępne pod adresem http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Następnie mamy wybór, w jakim formacie otrzymamy wyniki. Skupimy się na XML, ponieważ API w tym przypadku pozwala na korzystanie z filtracji.
Spróbujmy po prostu uzyskać listę wszystkich uruchomień zadania. Interesuje nas tylko nazwa kompilacji (displayName) i jej wynik (result):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Udało się?
Teraz odfiltrowujemy tylko te uruchomienia, które zakończyły się wynikiem SUCCESS. Użyjemy argumentu &exclude i jako parametr przekażemy mu ścieżkę do wartości różnej od SUCCESS. Tak, tak. Podwójne zaprzeczenie - to twierdzenie. Wykluczamy wszystko, co nas nie interesuje:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Zrzut ekranu listy udanych

No i dla pewności upewnimy się, że filtr nas nie oszukał (filtry nigdy nie kłamią!) i wyświetlimy listę "nie-udanych":
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Zrzut ekranu listy nie-udanych

Lista wersji z folderu na zdalnym serwerze
Jest i drugi sposób na uzyskanie listy wersji. Podoba mi się nawet bardziej niż wywołanie API Jenkins. Otóż, jeśli aplikacja została pomyślnie skompilowana, oznacza to, że została spakowana i umieszczona w repozytorium w odpowiednim folderze. Cóż, repozytorium to domyślnie magazyn roboczych wersji aplikacji. Zatem zapytamy go, jakie wersje są przechowywane. Zdalny folder będziemy uzyskiwać za pomocą curl, grep i awk. Jeśli kogoś interesuje jednowierszowa komenda, to jest pod spoilerem.
Komenda w jednej linii
Zwróć uwagę na dwie rzeczy: przekazuję w nagłówku dane do połączenia i nie potrzebuję wszystkich wersji z folderu, wybieram tylko te, które zostały stworzone w ciągu ostatniego miesiąca. Edytuj komendę zgodnie ze swoimi realiami i potrzebami:
curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//g' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/']+ )Konfiguracja zadań i plik konfiguracyjny zadania w Jenkinsie
Rozwiązaliśmy już źródło listy wersji. Teraz wkręcimy otrzymaną listę do zadania. Dla mnie oczywistym rozwiązaniem było dodanie kroku w zadaniu budowy aplikacji. Kroku, który byłby wykonywany w przypadku wyniku "sukces".
Otwieramy ustawienia zadania budowy i przewijamy na sam dół. Klikamy na przyciski: Add build step -> Conditional step (single). W ustawieniach kroku wybieramy warunek Current build status, ustawiamy wartość SUCCESS, wykonywana akcja w przypadku sukcesu Run shell command.
A teraz najciekawsza część. Konfiguracje zadań Jenkins są przechowywane w plikach. W formacie XML. Pod ścieżką http://ścieżka-do-zadania/config.xml W związku z tym można pobrać plik z konfiguracją, edytować go w potrzebny sposób i umieścić w miejscu, skąd został wzięty.
Pamiętacie, wcześniej uzgodniliśmy, że dla listy wersji stworzymy parametr. BUILD_VERSION?
Pobierzmy plik konfiguracyjny i zajrzyjmy do środka. Po prostu, aby upewnić się, że parametr jest na miejscu i rzeczywiście ma odpowiedni wygląd.
Zrzut ekranu pod spoilerem.
Wasz podany fragment config.xml powinien wyglądać tak samo. Z wyjątkiem tego, że zawartość elementu choices na razie brakuje.

Upewniliście się? No dobrze, piszemy skrypt, który będzie się wykonywał w razie pomyślnej kompilacji.
Skrypt będzie pobierał listę wersji, ściągał plik z konfiguracją, wprowadzał do niego w odpowiednie miejsce listę wersji, a potem kładł go z powrotem. Tak. Wszystko się zgadza. Wprowadzać listę wersji do XML’a w to miejsce, gdzie już jest lista wersji (będzie w przyszłości, po pierwszym uruchomieniu skryptu). Wiem, że na świecie znajdują się zagorzali miłośnicy wyrażeń regularnych. Ja do nich nie należę. Proszę zainstalować na tej maszynie, na której będzie edytowany plik konfiguracyjny. Wydaje mi się, że to niewielka cena, aby uniknąć edytowania XML za pomocą sed’a.
Pod spoilerem przedstawiam kod, który wykonuje opisany wyżej proces w całości.
Piszemy do pliku konfiguracyjnego listę wersji z folderu na zdalnym serwerze.
#!/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 '%sn' "${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.xmlJeśli bardziej spodobała Ci się opcja z pobieraniem wersji z Jenkins’a i jesteś tak leniwy jak ja, to pod spoilerem ten sam kod, ale lista z Jenkins’a:
Piszemy do pliku konfiguracyjnego listę wersji z Jenkins’a.
Tylko pamiętaj: u mnie nazwa kompilacji składa się z numeru porządkowego i numeru wersji, oddzielonych dwukropkiem. W związku z tym awk odcina niepotrzebną część. Dostosuj tę linię do swoich potrzeb.
#!/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 '%sn' "${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.xmlTeoretycznie, jeśli przetestowałeś kod napisany na podstawie powyższych przykładów, to w zadaniu na wdrożenie powinna już pojawić się rozwijana lista z wersjami. Tak mniej więcej jak na zrzucie ekranu pod spoilerem.
Prawidłowo wypełniona lista wersji

Jeśli wszystko zadziałało, to skopiuj skrypt do Run shell command i zapisz zmiany.
Podłączenie do Cloud shell
Nasze kontenery są na naszych serwerach. Jako środek dostarczania aplikacji i menedżer konfiguracji używamy Ansible. W związku z tym, gdy mówimy o budowie kontenerów, przychodzi mi do głowy trzy opcje: zainstalować Docker w Dockerze, zainstalować Docker na maszynie z Ansible lub budować kontenery w chmurze konsoli. O pluginach do Jenkins w tej notatce postanowiliśmy nie wspominać. Pamiętasz?
Postanowiłem, że skoro kontenery "z pudełka" można budować w chmurze konsoli, to po co komplikować sprawy? Zachowajmy to w prostocie, prawda? Chcę budować kontenery przy pomocy Jenkins w chmurze konsoli, a następnie wystrzeliwać je prosto do Kubernetes. Zwłaszcza, że w wewnętrznej infrastrukturze Google'a można liczyć na naprawdę szybkie połączenia, co korzystnie wpłynie na czas wdrożenia.
Aby połączyć się z chmurą konsoli, potrzebne są dwie rzeczy: gcloud i uprawnienia dostępu do Google Cloud API dla tego konkretnego VM, z którego to połączenie będzie nawiązywane.
Dla tych, którzy planują połączyć się zupełnie nie z Google Cloud
Google pozwala na wyłączenie interaktywnej autoryzacji w swoich usługach. Pozwoli to na połączenie się z konsolą nawet z ekspresu do kawy, o ile działa na *nix i sama ma konsolę.
Jeśli potrzebujesz, żebym bardziej szczegółowo omówił ten temat w ramach tej notatki — pisz w komentarzach. Jeśli uzbiera się wystarczająca liczba głosów, napiszę aktualizację w tej sprawie.
Najprostszy sposób na nadanie uprawnień — przez interfejs webowy.
- Zatrzymaj VM, z którego w przyszłości będzie wykonywane połączenie z chmurą konsoli.
- Otwórz informacje o VM i kliknij Edytuj.
- Na samym dole strony wybierz zakres dostępu VM Pełny dostęp do wszystkich Cloud API.
Zrzut ekranu

- Zapisz zmiany i uruchom VM ponownie.
Po zakończeniu ładowania VM, połącz się z nią przez SSH i upewnij się, że połączenie działa bez błędów. Użyj polecenia:
gcloud alpha cloud-shell ssh Udane połączenie wygląda mniej więcej tak

Wdrożenie w GKE
Ponieważ dążymy do całkowitego przejścia na IaC (Infrastruktura jako Kod), pliki Docker przechowujemy w Gicie. To z jednej strony. A wdrożenie w Kubernetes opisuje plik YAML, który jest używany tylko w tym zadaniu, który sam w sobie również jest kodem. To z drugiej strony. Ogólnie rzecz biorąc, mam na myśli, że plan jest taki:
- Bierzemy wartości zmiennych BUILD_VERSION i opcjonalnie wartości zmiennych, które będą przekazywane przez ENV.
- Pobieramy plik Docker z Gita.
- Generujemy YAML do wdrożenia.
- Przesyłamy te dwa pliki za pomocą SCP do konsoli chmurowej.
- Budujemy kontener na miejscu i przesyłamy go do rejestru kontenerów.
- Zastosowujemy plik wdrożenia obciążenia w Kubernetesie.
Przejdźmy do konkretów. Skoro mówimy o ENV, załóżmy, że musimy przekazać wartości dwóch parametrów: PARAM1 i PARAM2. Dodajemy ich zadanie do wdrożenia, typ — Parametr typu String.
Zrzut ekranu

Będziemy generować YAML prostym przekierowaniem echo do pliku. Zakłada się oczywiście, że w pliku Docker występuje PARAM1 i PARAM2, a nazwa obciążenia będzie awesomeapp, a zbudowany kontener z aplikacją wskazanej wersji znajduje się w Container registry po ścieżce gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, gdzie $BUILD_VERSION została wybrana z rozwijanej listy.
Lista poleceń
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.yamlAgent Jenkins po połączeniu przy użyciu gcloud alpha cloud-shell ssh tryb interaktywny nie jest dostępny, dlatego przekazujemy polecenia do konsoli chmurowej za pomocą parametru —command.
Czyścimy domowy katalog w konsoli chmurowej ze starego pliku Docker:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"Umieszczamy świeżo pobrany plik Docker w domowym katalogu konsoli chmurowej za pomocą SCP:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~Budujemy, tagujemy i przesyłamy kontener do Container registry:
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"
Postępujemy w ten sam sposób z plikiem wdrożenia. Zwróć uwagę, że w poniższych poleceniach używane są fikcyjne nazwy klastra, do którego odbywa się wdrożenie (awsm-cluster) oraz nazwa projektu (awesome-project), w którym znajduje się klaster.
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"Uruchamiamy zadanie, otwieramy wyjście z konsoli i liczymy na udane zbudowanie kontenera.
Zrzut ekranu

A potem udane wdrożenie przygotowanego kontenera.
Zrzut ekranu

Celowo pominąłem konfigurację Ingress. Z jednego prostego powodu: raz ustawione na workload podane imię pozostanie sprawne, niezależnie od tego, ile wdrożeń o tym imieniu przeprowadzimy. A w ogóle, to już trochę poza historią.
Zamiast wyników
Wszystkie powyższe kroki można byłoby właściwie pominąć, po prostu instalując jakiś plugin do Jenkinsa, jest ich mnóstwo. Ale jakoś nie lubię pluginów. Chociaż z drugiej strony, sięgam po nie tylko w ostateczności.
A jeszcze po prostu lubię zgłębiać jakieś nowe dla mnie tematy. Tekst powyżej jest również sposobem na podzielenie się odkryciami, które zrobiłem, rozwiązując opisaną na początku zadanie. Chcę podzielić się tym z tymi, którzy, jak i nie są krwiożerczym wilkiem w devopsie. Jeśli moje odkrycia pomogą przynajmniej komuś — będę zadowolony.
Źródło: habr.com

