Scenariusz
porządek najlepiej zaczynać od redukcji. niby trywializm, a jednak często widzę, że osoby próbują najpierw 'porządkować’ a zapominają o tym pierwszym kroq – a przecież łatwiej jest uporządkować np. 1oo elementów, niż 2oo. w przypadq subskrypcji nie jest inaczej – wiele pozostaje bez właściciela (co było poprzednio) ale też wiele pozostaje pustych. w ramach pojedynczego tenanta subskrypcje pochodzą z różnych offeringów i największy bałagan jest z developerskimi wynalazkami z MSDN, MSDN Dev/Test, Trial czy Visual Studio, które mogą sobie powoływać na żądanie.
aby wyłączyć tą możliwość należy założyć ticket do wsparcia. nie ma samodzielnej metody kontroli. jedyne, co można zrobić samemu, to zdefiniować domyślną Management Group do której (wszystkie) subskrypcje będą wpadać. dzięki temu 'nasze’, z głównej umowy typu Enterprise czy MCA zakładamy tam, gdzie mają być, a pozostałe, z niewiadomych źródeł, można zautomatyzować – czy to polisami czy jakimś triggerem.
Czysty PowerShell
napisanie prostego skrypciq w PS, który przeliczy ilość zasobów i ładnie wyświetli listę, nie jest skomplikowane:
$subscriptions = Get-AzSubscription | ? state -ne 'disabled' $counts = @() foreach($sub in $subscriptions){ write-host "processing $($sub.name)..." -ForegroundColor grey try { Set-AzContext -SubscriptionObject $sub | Out-Null } catch { write-host "error accessing $($sub.Name)" -ForegroundColor Red $counts += [pscustomobject]@{ subscriptionName = $sub.Name subscriptionId = $sub.Id resourceCount = -1 } continue } $res = Get-AzResource $counts += [pscustomobject]@{ subscriptionName = $sub.Name subscriptionId = $sub.Id resourceCount = $res.count } } $counts | sort resourceCount | export-csv -nti c:\temp\ResourcePerSubscription.csv -Encoding utf8 &(convert-CSV2XLS C:\temp\ResourcePerSubscription.csv)
nie lubię one-linerów, lubię wiedzieć co się dzieje, i mieć kontrolę – dla tego skrypt może wydawać się niepotrzebnie rozbudowany… ale mi zajmuje ok 1o min żeby taki napisać, wszystko widzę, jak coś wywali to wiem gdzie, mogę to debugować – i koniec-końców czas zainwestowany w trochę więcej więcej kodu i jakąś podstawową obsługę błędu – zwraca mi się dość szybko. zwłaszcza, że pojedynczy przebieg takiego kodu może trwać całkiem sporo – w moim przypadq to ok. 10 minut….
Przyspieszamy
… i właśnie ten czas jest denerwujący. oczywiście – niby takie rzeczy robi się bardzo rzadko, więc optymalizacja jest trochę sztuką-dla-sztuki. dla mnie jest sztuką-dla-nauki. języki operujące na zbiorach danych – jak SQL czy KQL – nie są dla mnie codziennością. warto się więc pogimnastykować, bo to z kolei zwróci się w przyszłości. tak było w przypadq samego PS – przedstawiony wcześniej skrypt, kilka lat temu, pewnie zająłby mi kilka razy więcej czasu. dziś z automatu wiem gdzie wrzucić obsługę błędu, a gdzie to jest strata czasu, kiedy warto zainwestować więcej, a kiedy walnąć one-liner’a.
spróbujmy więc uzyskać podobny efekt korzystając z KQL. największym problem dla myślących proceduralnie czy sekwencyjnie, jest przestawienie głowy na pracę na zbiorach. dobrze jest sobie zrobić powtórkę z matematyki, w ramach gimnastyki zwojów.
na początq szkielet, którego używam jako starter, i zmieniam sobie tylko kwerkę:
$searchParams=@{ first = 1000 query = $QUERY } $allResults = @() do { $results = Search-AzGraph @searchParams $results|%{$allResults+=$_} $searchParams.ContainsKey('skip') ? ($searchParams.skip+=1000) : $searchParams.add('skip',1000) } while($results.skipToken) $allResults
#1
najpierw prosty test:
$QUERY = "resources | summarize nrOfResources=count() by subscriptionId"
ten przykład zwraca ładnie wyniki – ilość subskrypcji wraz z ilością zasobów. to czego jednak NIE zwraca, to nazwy subskrypcji – a nam, humanoidom, jednak łatwiej posługiwać się literkami niż GUIDami. po drugie nie ma subskrypcji pustych. wynika to z faktu, że zapytanie jest do 'zbiór zasobów’ a potem grupuje je po atrybucie 'subscriptionId’. więc jeśli subskrypcja nie ma zasobów to nie ma zasobów do zaraportowania subskrypcji…
#2
ponieważ wynik ma być częścią wspólną zbioru subskrypcji oraz zbioru zasobów, najpierw napiszmy obie strony zapytania…
jako ciekawostka – jest pewna niespójność dotycząca zasobu/resource. chodzi o subskrypcje i ResourceGroupy, które zasadniczo, z definicji, nie są zasobami, a kontenerami zasobów. przez to mają swoją oddzielną domenę nazewniczą 'resourceContainers’. czemu niespójność? bo przy niektórych operacjach i commandletach traktuje się je jako zasoby. spojrzyj na samo zapytanie: ResourceContainers z microsoft.resources/subscriptions ?
…a że prawą stronę praktycznie napisaliśmy wcześniej, bo to po prostu zapytanie o zasoby, zajmijmy się lewą stroną:
$QUERY = "resourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName=name"
#3
do różnych operacji łączenia zbiorów służy, co dość oczywiste, 'join’. problem w tym, że jest wiele różnych joinów, a przypadq Kusto jest ich wyjątkowo dużo.
KQL – to Kusto Query Language. czasem łatwiej coś wyszukać korzystając z tego słowa kluczowego… przynajmniej póki search nie jest podpięty pod GPT (;
co więcej, domyślnym operatorem, jest [nomen-omen] unikalny dla KQL innerunique. to może być dość istotna różnica, więc może warto dodawać zawsze odmianę? tu na leniucha:
$QUERY = "resourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName=name | join resources on subscriptionId | summarize nrOfResources=count() by subscriptionId,subName | sort by nrOfResources asc"
#4
$QUERY = "resourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName=name | join kind=leftouter resources on subscriptionId | summarize nrOfResources=count() by subscriptionId,subName | sort by nrOfResources asc"
Returns all the records from the left side and only matching records from the right side.
#5
oto finalne rozwiązanie:
$QUERY = "resourceContainers | where type =~ 'microsoft.resources/subscriptions' | project subscriptionId, subName=name | join kind=leftouter (resources | summarize nrOfResources = count() by subscriptionId ) on subscriptionId | extend nrOfResources = coalesce(nrOfResources, 0) | sort by nrOfResources asc"