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 }}"