Azure DevOps pipelines - tips & tricks (1)

2022-05-19

Podstawową zasadą, którą wpaja się wszystkim młodym programistom, jest reguła reużywania kodu. Wiadomo - zamiast robić przeklęte ctrl+C i ctrl+V, lepiej jest zbudować reużywaną funkcję/moduł, dzięki czemu później zmianę wystarczy zrobić w jednym miejscu. Tę regułę można też zastosować do pipeline’ów YAML-owych w AzureDevops. W tym odcinku devopsowych tips & tricks, przyjrzymy się, jakie są opcje.

Template nesting

Pierwsza opcja to “zagnieżdżanie” szablonów, czyli “referencjonowanie” za pomocą template:

steps:
  - script: echo "Hello world"
  - template: ./nested-template.yml

Jest to podstawowa opcja, oczywiście możemy przekazać do takiego zagnieżdżonego szablonu parametry. Co ciekawe, szablon może znajdować się w innym repozytorium - wówczas musimy zdefiniować owo repozytorium jako zasób w sekcji resources i potem po nazwie zasobu odwołać się do szablonu, np. nested-template.yml@yaml-repo. Takie rozwiązanie nie wydaje się naturalne (raczej chcemy trzymać YAMLe pipeline’ów obok aplikacji), ale przy większych projektach i/lub organizacjach możemy mieć np. bazowe szablony dla wielu projektów/zespołów w oddzielnym repozytorium, którym opiekuje się dedykowany zespół.

Oficjlanie istnieją pewne ograniczenia ilości zagnieżdżeń oraz łącznego rozmiaru wszystkich YAMLów biorących udział w buildzie (zainteresowanych odsyłam do dokumentacji)- ale mnie się nie udało zderzyć ze ścianą. (Próbowałem dojść do limitu zagnieżdżeń.)

Pętle

Kolejną konstrukcją, którą adept programowania poznaje na wczesnym etapie, a którą też da się zastosować w pipeline’ach są pętle. Tutaj nie ma specjalnej filozofii: możemy iterować po listach. (Aczkolwiek de facto nie ma typu danych “lista” w Azure DevOps, no ale typy danych to może temat na oddzielny odcinek.) Przykładowo:

parameters:
  - name: characters
    type: object
    default:
      - Tytus de Zoo
      - Romek
      - A'Tomek

  - steps:
      - ${{ each character in parameters.characters }}:
          - script: echo "Hi, ${{ character }}"

Przy pisaniu trzeba tylko uważać na wcięcia i ważny dwukropek na końcu linijki, w której stosujemy słówko kluczowe each.

Matrix strategy

Bardziej wymyślną pętlą, zastosowaną na całym jobie i bardziej explicite wyrażoną jest owo matrix strategy. Zasada jest tu taka, że na poziomie joba określamy scenariusze jego uruchomienia, tzn. dla każdego scenariusza definiujemy wartość zmiennych, uzyskująć w ten sposób efekt podmiany ich wartości dla każdego scenariusza. Spójrzmy:

jobs:
  - job: MatrixSample
    strategy:
      matrix:
        v1:
          url: https://somwhere.com/old/api
        v2:
          url: https://somwhere.com/new/api

    steps:
      - bash: curl ${{ url }}

W powyższym przykładzie job wykona się dwa razy, dla scenarisuzy v1 i v2, które wykonają request dla starej i nowej wersji api. Oczywiście tutaj przykład wydumany, API powinno być wersjonowane explicite w URL czy w nagłówku, ale tak chyba lepiej widać, o co chodzi. Sam Microsoft w dokumentacji podaje przykład, że matrix strategy może być używane np. do uruchamiania testów na różnych platformach. O tym będzie, ale w innym odcinku. :)

Extend template

Ostatnia opcja to roszerzanie szablonów za pomocą słówka kluczowego extend. Jest do de facto dziedziczenie po innym szablonie. Może być przydatne w scenariuszu opisanym wyżej, czyli: bazowe szablony, wspólne dla wielu projektów, umieszczamy w innym repozytorium, a lokalnie dziedziczymy po nich i konstruujemy własne pipeline’y.

Jak działa to dziedziczenie? W podstawowej opcji, jest to po prostu odwołanie się do szablonu bazowego i przekazanie parametrów. Czyli prawie to samo, co zagnieżdżanie, tyle że robione na poziomie całego szablonu.

# base.yml
parameters:
  - name: yourName
    type: string

steps:
  - script: echo "Hello, ${{ parameters.yourName }}, I am base template".
# extending.yaml
extend: base.yml
  parameters:
  - yourName: Robert

Sam Microsoft w dokumentacji sugeruje, że ten mechanizm można wykorzystać jak prawdziwe “dziedziczenie”: skoro typem parametru może być lista stepów, to dlaczego nie zaprojektować bazowego szablonu tak, aby przyjmował taki opcjonalny parametr. W ten sposób możemy zaimplementować odpowiednik funkcji wirtualnej lub abstrakcyjnej - konstruktów z programowania obiektowego.

# base.yml
parameters:
  - name: yourName
    type: string
  - name: preSteps
    type: stepList
    default: [] # thanks to default value this is 'virtual'
  - name: postSteps
    type: stepList

steps:
  - ${{ each step in parameters.preSteps}}:
      - ${{ step }}
  - script: echo "Hello, ${{ parameters.yourName }}, I am base template".
  - ${{ each step in parameters.postSteps}}:
      - ${{ step }}
# extending.yaml
extend: base.yml
  parameters:
  - yourName: Robert
  - postSteps:
    - script: echo "Goodbye!"

Podsumowanie

Jak widać, przy głębszym poznaniu, AzureDevops oferują całkiem niezłe konstrukty do budowania zaawansowanych szablonów. Co więcej, dokumentacja jest całkiem nieźle napisana i zawiera przykłady. Z własnej praktyki mogę przyznać, że dotychczas “produkcyjnie” użyłem wszystkich wyżej wymienionych opcji, prócz matrix strategy - czyli nie są to tylko zabawki, ale realne pomoce, które warto mieć w swojej skrzynce narzędziowej.


Robert Skarżycki - zdjęcie profilowe

Pisanina, której autorem jest Robert Skarżycki - programista .NET, mąż szczęśliwej żony, rodzic
moje bio
mój Twitter
mój LinkedIn
moje szkolenia i warsztaty

© 2022, Built with Gatsby & passion