Infrastructure as Code – Bicep

Azure - Bicep

Einführung

Genau wie der Bizeps den Arm verschönert, ist Bicep auch eine Verschönerung von ARM-Templates.  
Im Grunde bietet es die gleichen Funktionen wie ARM-Templates, soll aber dennoch leichter zu lesen sein, da es nicht auf JSON basiert. 

Was ist Bicep?

Bicep ist eine ARM-Vorlagensprache für das deklarative Bereitstellen von Azure-Resourcen 
Bicep ist eine Domain Specific Language (DSL), was bedeutet, dass sie für eine bestimmte Domäne entworfen wurde. 

Wie wird das Tool genutzt?

In diesem Beitrag benutze ich Visual Studio Code (VS Code) und die Azure CLI, um Bicep Dateien zu erstellen und in Azure bereitzustellen. Dazu brauchen wir die Bicep-Extension um die Vorlagen zu erstellen. 

Und wir benötigen die Azure CLI, um die Ressourcen in der Vorlage bereitzustellen. 

 

Um eine Bicep-Vorlage zu erstellen, muss erstmal eine Bicep-Datei erstellt werden. Man kann ganz einfach in VS Code eine Datei mit der Dateiendung FileName.bicep erstellen.  

Wenn man dann ein Bicep-Template erstellt hat und bereitstellen will, sollte man erst einmal überprüfen ob die Ressourcen erzeugt werden können. Dazu gibt man folgendes im Terminal ein: 
 

az deployment group validate -g ResourceGroupName –template-file FileName.bicep 

 

Wenn keine Fehlermeldung kommt und man die Ressourcen nun erzeugen will, gibt man folgendes ein:  

az deployment group create -g ResourceGroupName –template-file FileName.bicep 

  

Man kann eine Bicep-Vorlage auf verschiedenen Ebenen bereitstellen, innerhalb von Ressourcengruppen, Subscriptions, ManagementGroups und TenantsStandardmäßig wird die Ressourcengruppe als Ebene benutzt. 

Dateistruktur einer Bicep-Vorlage

Eine Bicep Datei besitzt keine spezifische Vorlage wie beim JSON ARM-Template, sie hat aber eine gewisse Dateistruktur. In Bicep-Vorlagen kann man folgende Datentypen verwenden: arrayboolintObjectsecureObjectsecureString und string 
Näheres zu den Datentypen kann man HIER finden. 

Parameter

Parameter sind Werte, die vom Entwickler vorgegeben werden können, um die Bereitstellung anzupassen. Es gibt auch die Möglichkeit eine Parameter Datei zu erstellen, um dort alle Parameter übersichtlich zu sammeln.  
An jedem Parameter hängt optional ein sog. Decorator, dies sind Bedingungen zum Parameter. In unserem Beispiel weiter unten im Text stellen wir z.B. die Bedingung, dass der Parameter maximal 11 Zeichen lang sein darf. Die Basis-Syntax von Decorators ist wie folgt: 

				
					@<decorator>(<argument>) 
param <parameter-name> <parameter-data-type> = <default-value> 
				
			

Variables

Variablen werden benutzt, um Ausdrücke in der Vorlagensprache zu vereinfachen. Der Datentyp bei Variablen wird von dem Wert abgeleitet. Man sollte Variablen nur bei Werten benutzen, die sich oft im Template wiederholen. 

				
					var <variable-name> = <variable-value> 
				
			

Resources

Um Ressourcen zu erzeugen, braucht es einen logischen Namen(resourcesymbolic-name) für die Ressourcesowie einen Ressourcentyp und eine APIVersion. Diese werden aber für die meisten Ressourcen durch die IntelliSense Unterstützung von der Bicep-Extension für VS Code schon vorgegeben. 
Der logische Name ist dafür da, um Ressourcen logisch im Template voneinander abgrenzen zu können. Dies ist z.B. notwendig, um Ressourcen gleichen Typs mit verschiedenen Konfigurationseinstellungen erstellen zu können. 

				
					resource <resource-symbolic-name> '<resource-type>@<api-version>' = { 
  <resource-properties>
} 
				
			

Functions

Bicep bietet – anders als ARM-Templates – keinen Block zum Erstellen von benutzerdefinierten Funktionen an, man kann aber dennoch vordefinierte Funktionen benutzen. Mehr dazu unter dem Link.

Outputs

Outputs benutzt man, um Werte aus der Bereitstellung zurückzugeben.

				
					output <output-name> <output-data-type> = <output-value> 
				
			

Anhand eines kleinen Beispiels, in dem ich einen Storage Account erzeuge und den Connection String ausgeben lasse, kann man besser verstehen, wie eine Bicep Datei aufgebaut ist / geschrieben wird. 

				
					@maxLength(11) 

param storagePrefix string
param storageSKU string = 'Standard_LRS' 
param location string = resourceGroup().location 

var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}' 


resource storage 'Microsoft.Storage/storageAccounts@2021-04-01' = { 
    name: uniqueStorageName 
    location: location 
    sku: { 
        name: storageSKU 
    } 
    kind: 'StorageV2' 
    properties: { 
        supportsHttpsTrafficOnly: true 
    } 
} 

output storageName string = storage.name 

// Determine our connection string 
var StorageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storage.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage.id, storage.apiVersion).keys[0].value}' 

// Output our variable 
output StorageAccountConnectionString string = StorageConnectionString 
				
			

Vergleicht man die ARM-Vorlage vom letzten Blog-Beitrag mit dieser Bicep-Vorlage, sieht man, dass die gleichen Funktionen ausgeführt werden, aber die Bicep-Datei um einiges kürzer und einfacher zu lesen ist. 

Ein cooles Feature von Bicep ist, dass Bicep dazu fähig ist, auf Daten von Ressourcen zuzugreifen, die noch nicht existieren. 

In unserem Beispiel greift Bicep direkt auf die Account Keys von unserem noch nicht existierenden Storage Account zu; so leicht ist das in anderen Infrastructure-As-Code-Tools nicht immer. 

Beispiel: Erzeugen eines Modern Data Warehouse

Wie am Anfang erwähnt, erzeuge ich ein Modern Data Warehouse und ergänze unser Beispiel oben mit dem Storage Account um einen Container, eine Azure Data Factory, einen SQLServer sowie eine SQL-Datenbank. Der Code für unser Beispiel sieht dann wie folgt aus: 

				
					@description('Specifies the name of the Azure Storage account.') 
@maxLength(11) 
param storagePrefix string 

@description('Specifies the name of the blob container.') 
param containerName string  

@description('Specifies the name of the Azure Data Factory.') 
param dataFactoryPrefix string 

@description('Specifies the name of the Azure SQL-Server.') 
param SQLserverPrefix string 

@description('Specifies the Login to SQL-Server.')  
param administratorLogin string 

@description('Specifies the Login Password to SQL-Server.') 
param administratorLoginPassword string 

@description('Specifies the name of the Azure SQL-DB.') 
param SQLDBPrefix string 


var location = resourceGroup().location 

var uniqueStorageName = '${storagePrefix}${uniqueString(resourceGroup().id)}' 

var uniqueDataFactoryName = '${dataFactoryPrefix}${uniqueString(resourceGroup().id)}' 

var uniqueSQLServerName = '${SQLserverPrefix}${uniqueString(resourceGroup().id)}' 

var uniqueSQLDBName = '${SQLDBPrefix}${uniqueString(resourceGroup().id)}' 

 
resource storage 'Microsoft.Storage/storageAccounts@2021-04-01' = { 
  name: uniqueStorageName 
  location: location 
  sku: { 
    name: 'Standard_LRS' 
  } 
  kind: 'StorageV2' 
  properties: { 
  } 
} 

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-04-01'={ 
  name : '${storage.name}/default/${containerName}' 
  dependsOn:[ 
    storage 
  ] 
} 

resource adf 'Microsoft.DataFactory/factories@2018-06-01' = { 
  properties: {} 
  name: uniqueDataFactoryName 
  location: location 
  identity: { 
    type: 'SystemAssigned' 
  } 
} 

resource SQLServer 'Microsoft.Sql/servers@2021-02-01-preview' = { 
  name: uniqueSQLServerName
  location: location 
  properties: { 
    administratorLogin: administratorLogin 
    administratorLoginPassword: administratorLoginPassword 
  } 
} 

resource SQLDB 'Microsoft.Sql/servers/databases@2021-02-01-preview' = { 
  properties: {} 
  name: '${SQLServer.name}/${uniqueSQLDBName}' 
  location: location 
  sku: { 
    name: 'Standard' 
    tier: 'Standard' 
  } 
  dependsOn:[ 
      SQLServer 
  ] 
} 


// Determine our connection string 
var StorageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storage.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storage.id, storage.apiVersion).keys[0].value}' 

// Output our variable 
output StorageAccountConnectionString string = StorageConnectionString 

output StorageName string = storage.name 
				
			

Fazit

Zum Abschluss dieses Blogbeitrages erläutere ich die Vor- und Nachteile von Bicep-Templates.  

Wie bei ARM-Templates eignet sich Visual Studio Code zur Entwicklung von Bicep-Vorlagen. Man hat einige grundsätzliche Vorteile dabei, wie unter anderem eine Quellcodeverwaltung 
Des weiteren bietet die Erweiterung, die man benötigt, viele weitere Vorteile, wie zum Beispiel IntelliSense zur Deklarierung von Ressourcen. Durch die Erweiterung kann man sich auch die Bereitstellung visualisieren lassen, als Beispiel für eine Visualisierung hier die Bereitstellung für unser MDWH.  

Ebenso wie ARM-Templates unterstützen Bicep-Vorlagen den, “Day-Zero”-Support, was bedeutet, dass man neue Ressourcen oder Features, sobald diese im Azure Portal verfügbar sind, auch direkt über Bicep bereitstellen kann.   
Bei Verwendung der Azure-CLI unterstützen Bicep-Bereitstellungen die validate-Funktion, mit der man vor der Bereitstellung in Sekundenschnelle herausfinden kann, ob Fehler entstehen, könnten.  
Wenn man bereits ARM-Vorlagen hat und mit Bicep weiterarbeiten möchte, kann man durch die Azure CLI ARM-Vorlagen zu Bicep-Vorlagen automatisch transformieren: 
az bicep decompile –file fileName.json 

Man kann sagen, dass die Vorteile, die ARM-Vorlagen bereits haben, auch zu den Vorteilen von Bicep-Vorlagen gehören. Einige der Nachteile von ARM werden durch Bicep gelöst, wie zum Beispiel der Nachteil, dass ARM-Vorlagen so schwer zu lesen sind. Bicep-Vorlagen sind eindeutig einfacher zu lesen, und sind kürzer vom Umfang.  

So schön die Vorteile sein mögen, haben auch Bicep-Vorlagen Nachteile. 
Ein sehr großer Nachteil ist, dass man Bicep (wie auch ARM), nur in Azure benutzen kann. Dieses Tool ist daher ebenso, wie ARM also nur für solche Entwiclungs-Teams interessant, die sich fest auf diesen Cloud Provider eingeschossen haben. 
Ebenso ein wichtiger Punkt, weshalb ich mich gegen Bicep entschieden habe, war der Fakt, dass man während der Bereitstellung keine lokalen Dateien in einen Storage Container hochladen kann; eine Beschränkung, die. auch bei ARM vorliegt.