Een Azure Resource Management en Azure DevOps puzzel voor webapp devs
Het uitgangspunt van deze case is een Azure SQL database in een resource group. Er is één webapplicatie (binnen diezelfde resource group), die gebruik maakt van de database. Om de webapplicatie toegang te geven tot de database kan binnen Azure eenvoudigweg een switch worden omgezet naar “ON”:
Deze optie geeft Azure services toegang tot SQL server (databases). Dit zijn niet alleen de Azure services binnen de resource group, of alleen de services binnen de Azure subscription, maar alle Azure services. Een ongewenste situatie. Graag willen we SQL server afschermen, zodat alleen de webapplicatie toegang heeft tot de SQL server (database).
Een appservice krijgt meerdere outbound IP adressen toegewezen.
De outbound IP adressen kunnen worden toegevoegd in de firewall rules van de SQL server, maar deze IP adressen zouden weleens kunnen wijzigen.
De Azure resources en de webapplicatie worden automatisch uitgerold door middel van ARM templates in combinatie met Azure DevOps. Na een uitrol zijn alle gegevens beschikbaar om de rules in te stellen. De wens is om ook de firewall rules automatisch in te stellen, door de gegevens van de appservice resource en SQL server resource tijdens de uitrol aan elkaar te koppelen.
De technische implementatie
In de Visual Studio solution van de webapplicatie is een Azure Resource Group project opgenomen met daarin twee ARM templates. Wanneer alle bestanden in source control zijn geplaatst wordt de oplossing met Azure DevOps gebuild en gereleased.
ARM templates
Het eerste ARM template (azuredeploy.json) rolt alle individuele Azure resources uit, waaronder de appservice en de SQL server instance. De outbound IP adressen van de appservice zijn opvraagbaar en worden, samen met de naam van de SQL resource, geëxporteerd via de outputs sectie van het template:
Het tweede ARM template (addsqlfirewallrules.json) krijgt als input parameter een array van IP adressen en de naam van de SQL server resource. Vervolgens wordt op iteratieve wijze (copy, count, index) de inhoud van de array toegevoegd aan de firewall rules.
Azure DevOps
De volgende taken zijn gebruikt in de release om de appservice uit te rollen:
Bij het uitrollen van het ARM template is een naam opgegeven voor de variabele waarin de outputs van het ARM template (als string) ingezet moet worden:
Output van deze taak:
Voeg een PowerShell inline script toe om de variabele met ARM outputs om te vormen naar environment variabelen die in opeenvolgende taken kunnen worden gebruikt.
Maak van de string weer een object:
$armoutput = ‘$(armoutputstring)’ | convertfrom-json
Bewaar de waarden waarin je geïnteresseerd bent in een variabele. Gebruik ConvertTo-Json -Compress, zodat de variabele $ips een string array bevat:
$ips= ($armoutput.outboundIpAddresses.value | ConvertTo-Json -Compress)
$sql = $armoutput.sqlResourceName.value
Maak van deze variabele een environment variabele, zodat je deze in een volgende(!) taak kunt gebruiken. Je kunt de waarde niet in de huidige taak evalueren. Let op het gebruik en de plaatsing van de quotes:
Write-Host “##vso[task.setvariable variable=armoutboundips;]$ips”
Write-Host “##vso[task.setvariable variable=armsql;]$sql”
Mocht je de inhoud van de lokale variabele willen bekijken:
Write-Host “app service outbound ip array: $ips”
De output is dan:
De volgende taak is niet vereist voor de release. Het geeft feedback of de variabelen, die je zojuist gezet hebt, ook inderdaad bruikbaar zijn:
De output van deze taak, merk op dat de dubbele quotes in deze weergave verdwenen zijn, dit heeft verder geen effect.
Let op dat de nieuwe environment variabele alleen in hoofdletters beschikbaar is:
Write-Host “app service outbound ip array: $(ARMOUTBOUNDIPS)”
Nu kan de array met IP adressen worden doorgegeven als parameter aan het volgende ARM Template, dat de IP nummers toevoegt als rule in de SQL firewall.
-sqlResourceName $(ARMSQL) -outboundIpAddresses $(ARMOUTBOUNDIPS)
Waarom deze implementatie?
Op zich is het een eenvoudige implementatie en binnen DevOps is het mogelijk op deze manier de twee acties achtereenvolgend uit te voeren. Maar omdat het één uitrol betreft, binnen één resource group, lijkt het logischer om alles enigszins bij elkaar te houden. Dit was initieel de intentie, maar onderweg richting de eindoplossing waren daar een aantal beren op de weg.
Leesbaarheid/onderhoudbaarheid
Wellicht is het mogelijk binnen het ARM template om de firewall rules en/of SQL resource afhankelijk te maken (dependson) van de appservice resource en vervolgens direct over de outbound IP adressen te itereren. Dit heb ik niet geprobeerd omdat ik niet dacht dat dit de leesbaarheid/onderhoudbaarheid van het ARM template ten goede zou komen, als dit al foutloos zou lukken. Het is namelijk niet mogelijk gedurende een ARM template uitrol de outbound IP adressen op te vangen in een variabele. Je hebt alleen de mogelijkheid om de resource properties uit te vragen.
Op alle plekken waar op dit moment in addsqlfirewallrules.json (zie boven) parameters(‘outboundIpAddresses’) staat, zou dit vervangen moeten worden door:
Linked Template (met link)
Na een ARM template uitrol kun je direct een tweede uitrol laten starten door een deployment resource te definiëren en deze te laten verwijzen naar een linked ARM template.
Absolute of relatieve paden worden niet geaccepteerd als template link. Het moet persé een webadres zijn. In het ARM template kun je de url van het template vanuit de deployment context opvragen. De template link zou dan als volgt zou kunnen zijn:
“[uri(concat(deployment().properties.templateLink.uri, ‘addsqlfirewallrules.json?’), ”)]”
Dit werkt echter niet bij een deployment vanuit Visual Studio of vanuit Azure DevOps.
In dat geval zou het tweede ARM template in BlobStorage geplaatst kunnen worden.
“[concat(‘https://’,variables(‘blobStorageResourceName’),’.blob.core.windows.net/’, ‘blobContainerName/addsqlfirewallrules.json’)]”
Maar dat maakt het nodeloos ingewikkeld, ook om bijvoorbeeld toegang tot het bestand in te regelen (SAS token), aangezien we deze scripts en blobcontainer liever niet publiek toegankelijk willen maken.
Linked Template (inline)
Het tweede template kan ook integraal in het eerste worden gevoegd in plaats van een link naar een bestand te gebruiken.
Het is voor te stellen dat dit de leesbaarheid/onderhoudbaarheid van het ARM template ook niet ten goede komt. Er is wel een poging gewaagd. Maar door de onduidelijke foutmeldingen bij het releasen van het template hebben we deze poging gestaakt. Misschien is een extra stap nodig, maar daarmee wordt het wel overzichtelijker en beter te beheren.
ARMOutputs DevOps release taak
De ‘ARM Outputs’ taak wordt doorgaans gebruikt om de output parameters vanuit het ARM template beschikbaar te maken in de release taken. In dit geval was er een output parameter van het type Array. Ook daar kan deze taak mee omgaan. Echter, worden alle array entries omgezet tot losse variabelen.
In dit geval wilde ik de array als zodanig doorgeven aan het volgende ARM template. Gelukkig kun je tegenwoordig de outputs in de ‘Azure resource group deployment’ taak opvangen in een variabele en ze zelf met PowerShell uitvragen en omzetten tot environment variabelen.