Einführung in Azure Bicep – Teil 4

Modularisierung und Templateorganisation

Modularisierung und Templateorganisation

Einführung in Azure Bicep – Teil 4

Modularisierung und Templateorganisation


Mit diesem Beitrag setzen wir die Reihe über Infrastructure as Code (IaC) mit Azure Bicep, dem Nachfolger der klassischen ARM-Templates, weiter fort.

Im letzten Teil der Serie ging es zum einen darum, den Umgang mit bestehenden Ressourcen zu verstehen. Diese sind entweder im Vorfeld selbst oder von anderen bereits erstellt worden und müssen daher im Code referenziert werden, damit mit den Objekten der vorhandenen Services gearbeitet werden kann. Des Weiteren ging es um diverse Codegenerierungsoptionen – ohne irgendeinen Co-Pilot –, Konvertierungsmöglichkeiten zwischen Bicep und ARM-Remplates sowie Child- und Extension-Ressourcen.

In diesem Teil geht es nun um die verschiedenen Möglichkeiten, den Bicep-Code über Module zu strukturieren, diese zu speichern und bei Bedarf von diversen Quellen für die Orchestrierung in einem zentralen Template abzurufen. Wir klären, wie die Module erstellt und verwendet werden können. Außerdem soll verdeutlicht werden, was überhaupt in ein Modul gehört und wann ihre Anwendung wirklich Sinn macht.

Moduldefintion

Was genau ist nun ein Modul? Ähnlich wie bei Terraform handelt es sich dabei im Prinzip nur um eine weitere (Bicep-)Datei an einer anderen Stelle, z. B. in einem Ordner im Filesystem oder in einem anderen Repository.

Dieses File enthält Konfigurationen für ein logisch zusammenhängendes Deployment, beispielsweise mit Ressourcen, Variablen und Outputs. Wie viel davon im Modul definiert wird, ist technisch nicht limitiert und schwankt zwischen einzelnen Services oder kompletten Applikationsinfrastrukturen.

Die Datei selbst sieht dann im Prinzip genauso aus wie bisher – ein Schlüsselwort wie Module taucht dort nirgends auf. Genau wie bei Terraform braucht es dieses dann aber beim Ort der Orchestrierung, das heißt in der Datei, wo das Modul aufgerufen wird. Dort wird üblicherweise auch mehr als ein Modul integriert und die Option der Wiederverwendbarkeit eines Moduls in anderen Templates ist natürlich auch gegeben. Ein einfaches Beispiel wäre die Kombination aus App Service, Datenbank und Netzwerkkomponenten (Abb. 1).

hafermalz_bicep_teil4_1

Abb. 1: Möglicher Modulaufbau

Die Verwendung erfolgt schließlich mit der Angabe der Modulquelle anstatt einer Providerdefinition, dem Deployment-Namen, eventuellen Scope-Definitionen sowie der Übergabe von nötigen Parametern. Auch ein Modulaufruf hat einen symbolischen Namen, der innerhalb der Templates referenziert werden kann, um auf Eigenschaften oder Outputs zuzugreifen. Die Details dahinter wollen wir nun mit Hilfe der folgenden Beispiele klären.

Aufruf lokal aus dem Dateisystem

Starten wir mit einem simplen Aufruf eines Moduls, hinter dem sich zur guten Veranschaulichung lediglich ein Storage-Account verbirgt, abgelegt im eigenen Filesystem (Listing 1).

Listing 1

param location string = 'northeurope'

module storage '../modul1/storage.bicep' = {
  name: 'storage'
  params: {
    storagePrefix: 'sto007'
    location: location
  }
}

An dem referenzierten Ort liegt das Storage-Skript und es funktionieren die gleichen Deployment-Wege, wie wenn wir das Template selbst deployen wollten.

Anwendung in Schleifen und verschiedenen Scopes

Die Implementierung von Modulen bietet zusätzliche Flexibilität über eine Scope-Angabe im Aufruf, was deren Wiederverwendbarkeit erhöht. Das Scope Property ist sonst nur für Extension-Ressourcen verfügbar. Mit folgendem Beispiel lässt sich so ganz einfach die Ressourcengruppe für das aufgerufene Modul festlegen. Anders formuliert: Soll die gleiche Ressourcenkonfiguration direkt innerhalb eines Deployments in verschiedene Zielbereiche provisioniert werden, braucht es zwingend den Modulansatz. Außerdem lässt sich damit der Target-Scope überschreiben bzw. unterschiedlich setzen. Angenommen, im Orchestrierungsfile wird der Standard-Scope einer Ressourcengruppe für die Module verwendet. Es kann in der gleichen Datei auch ein Modul aufgerufen werden, welches im Scope einer Subscription provisioniert. managementGroup() und tenant() werden ebenfalls unterstützt. Der Vorteil davon wird bei Anwendung mit Schleifen noch deutlicher.

Verwendung in Schleifen

Mit Hilfe einer Auflistung kann auch beim Modulaufruf durch diese iteriert werden. In den Listen könnten beispielsweise Namen, Präfixe oder Scope-Bereiche wie Subscriptions oder Ressourcengruppen hinterlegt sein. Im folgenden Snippet wird ein Array von Präfixen für den Storage-Namen verwendet (Listing 2). Wichtig bei der Verwendung von Schleifen für Module ist die Eindeutigkeit pro Aufruf. Dies kann entweder durch verschiedene Deployment-Namen für das Modul oder abweichende Scopes bewerkstelligt werden – andernfalls wird das von Bicep moniert.

Listing 2

var stoPrefixes = ['001', '002', '003']

//per loop
//bei Schleifen muss durch name oder scope Eindeutigkeit gewährleistet sein
module storage3 '../modul1/storage.bicep' = [
  for prefix in stoPrefixes: {
    name: 'storageFor${prefix}'
    params: {
      storagePrefix: prefix
      location: location
    }
  }
]

Häufig soll das gleiche Modul in verschiedenen Scopes provisioniert werden, wie beispielsweise eine Ressourcengruppe in mehreren Subscriptions (Listing 3).

Listing 3

module resoureGroups 'modules/resourcegroup.bicep' = [for sub in subscriptionsList: {
  scope: subscription(sub)
  name: 'resource-group'
  params: {
    resourceGroupName: 'rg-alerts'
  }
}]

Ebenfalles möglich ist die praktische Kombination mit einer if-Schleife im Modulaufruf, wodurch nur bei entsprechender Bedingung das Deployment durchgeführt wird:

module expressroute_alerts 'modules/expressroute-alerts.bicep' = if (tenantInfo.displayName == 'Production Tenant') {
  name: 'expressroute-alerts'
  params: {
    ag_id: ag.outputs.ActionGroup_id...