Azure DevOps pipelines - tips & tricks (2)

2022-07-11

W dzisiejszym odcinku pochylimy się nad zmiennymi i parametrami. Poniższa tabelka przedstawia podstawowe różnice:

parametry zmienne
różne typy danych ✔️ ❌ tylko string
zmiana w runtime ✔️
kierunek przekazywania top-down ⬆️⬇️➡️

Warto o tych różnicach pamiętać, bo w wielu przypadkach może się wydawać, że w zasadzie możemy zmienne i parametery stosować zamiennie. Tymczasem rządzi nimi inna logika, a do tego - każda grupa ma swoje… dziwne pułapki.

Największe pułapki ze zmiennymi

Po pierwsze, zmienne są by default przekazywane do skryptów Bash/CMD. Wydaje się na to pierwszy rzut oka sensowne, tym bardziej, że “gratis” dostajemy w skryptach dostęp do zmiennych, które są dostarczone przez platformę, np. Build.BuildId. Ale to ma też swoje konsekwencje

Nie ma nadpisywania zmiennych przy użyciu taska script

Weźmy taki kawałek YAMLa:

variables:
  MY_VARIABLE: "original value"

steps:
  - bash: echo "$MY_VARIABLE"
    env:
      MY_VARIABLE: "overriden?"

W 5. linii wołamy kawałek skryptu Bash, który ma wydrukować na ekran wartość zmiennej środowiskowej MY_VARIABLE. Dla mnie logiczne było to, że skoro zmienna pipeline’owa MY_VARIABLE automatycznie jest wstrzykiwana do skryptu jako zmienna środowiskowa o tej samej nazwie, to jednak przekazanie jakiejś wartości w liście env spowoduje “wygraną” tej właśnie lokalnej wartości. Tak jednak nie jest. Powyższy kawałek kodu wydrukuje: original value. Przyznacie, że to mindfuck?

Zmienne typu sekret nie są przekazywane do skryptów

W variable groups możemy zdefiniować niektóre zmienne jako sekrety (chyba też wartości zaimportowane z key vaulta nimi są domyślnie), dzięki czemu uzyskujemy większe bezpieczeństwo w samym portalu: wartości sekretu nie można podejrzeć, ani skopiować, ani też zobaczyć jego użycia w logach konkrentego builda (wartość jest “wygwiazdkowana”). Niestety, trzeba pamiętać, że taka zmienna typu sekret nie zostanie automatycznie przekazana do skryptu Bash/CMD, co powoduje, że musimy je przekazać przez argument env.

Service connection nie może być w zmiennej

Jeśli mamy taką sytuację, że np. środowiska testowe i produkcyjne są na innych subskrypcjach Azure, to wówczas potrzebujemy dwóch oddzielnych service connections, aby móc np. wdrażać jakieś zasoby do chmury. Dobrze by było mieć takie rozgałęzienie w pipeline’ie, że w zależności od środowiska używamy konkretnego service connection. Czyli można by chcieć mieć coś takiego:

# dev-vars.yaml
variables:
  SERVICE_CONNECTION: "dev-service-connection"
# prod-vars.yaml
variables:
  SERVICE_CONNECTION: "PROD-service-connection"
# main.yaml
parameter:
  - name: environment
    type: string
    values:
      - dev
      - prod

stages:
  - stage:
    variables:
      - template: ${{ parameters.environment }}-vars.yaml
    steps:
    # -- omitted, but with same step using $(SERVICE_CONNECTION)

(Przy okazji - tutaj jeszcze jeden ciekawy przypadek użycia szablonów: jako “worków” ze zmiennymi, które możemy “importować” w stage’ach i jobach.) Niestety, tak się nie da: przy próbie użycia zmiennej SERVICE_CONNECTION np. w tasku AzureResourceGroupDeployment@2 - dostaniemy na twarz błąd, mówiący mniej więcej, iż “ten pipeline nie jest autoryzowany do service connection o nazwie ’$(SERVICE_CONNECTION)`“. A zatem widzimy od razu, że problem jest z rozwiązywaniem tej zmiennej. Niestety, jedyny workaround to wprowadzenie sztucznego parametru, który będzie służył za słownik:

# main.yaml
parameters:
  - name: environment
    type: string
    values:
      - dev
      - prod
  - name: serviceConnections
    type: object
    default:
      dev: "dev-service-connection"
      prod: "PROD-service-connection"

stages:
  - stage:
    variables:
      - SERCICE_CONNECTION: ${{ parameters.serviceConnections[parameters.environment] }}
    steps:
    # -- omitted, but with same step using $(SERVICE_CONNECTION)

To jest niestety hak, który w głowie miesza pomiędzy parametrami, które przychodzą z zewnątrz a parametrami, które są “internal use”. A żeby było jeszcze gorzej, to na portalu supportu Microsoftu jest na to zgłoszony błąd, który od… 3 lat czeka na rozwiązanie.

Runtime parameter nie może być opcjonalny

Siłą parametrów “najwyższego rzędu” - czyli parametrów, które definiujemy w tym YAML-u, który potem jest użyty do konkretnej build definition - jest to, że gratis dostajemy ładny UI do specyfikowania tychże dla konkretnego uruchomienia. W ten sposób przykładowo scenariusz z poprzedniego podrodziału mógłby być użyty tak, że wybór środowiska - dev bądź prod - dokonuje się z eleganckiego dropdowna. Wszystko jest fajnie dopóty, dopóki nie zechcemy wprowadzić parametru opcjonalnego. Chciałoby się użyć konsekwentnie tej samej składni, której używamy, czyniąc “zwykły” parametr opcjonalnym - a więc określając jego wartość w polu default; a w szczególnym przypadku tą wartością możę być pusty string.

# it does not work :(
parameters:
  - name: firstName
    type: string
  - name: optionalSecondName
    type: string
    default: "" # it does not work :(

steps:
  - ${{ if eq(parameters.optionalSecondName, '') }}:
      - script: echo "Hello, ${{ parameters.firstName }}"
  - ${{ else }}:
      - script: echo "Hello, ${{ parameters.firstName }} ${{ parameters.optionalSecondName }}"

Niestety, zamiast takiej oczywistej składni, musimy kombinować z wpisaniem jakiegoś słowa kluczowego w polu-które-ma-być-opcjonalne:

# it does work, but :(/
parameters:
  - name: firstName
    type: string
  - name: optionalSecondName
    type: string
    default: "I DO NOT HAVE ONE"

steps:
  - ${{ if eq(parameters.optionalSecondName, 'I DO NOT HAVE ONE') }}:
      - script: echo "Hello, ${{ parameters.firstName }}"
  - ${{ else }}:
      - script: echo "Hello, ${{ parameters.firstName }} ${{ parameters.optionalSecondName }}"

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