Infrastructure as Code – ARM

Azure Resource Manager - ARM

In meinem vorherigen Blogbeitrag zum Thema Infrastructure as Code (im folgenden IaC genannt) habe ich eine Einführung zum Thema gegeben. In den nächsten Beiträgen werde ich vier Tools vorstellen, die Vor- und Nachteile nennen und jeweils als Beispiel ein Modern Data Warehouse erzeugen. Was genau das ist und welche Ressourcen es dafür benötigt, wurde in meinen vorherigen Beitrag erklärt.  

Als erstes will ich über die in Azure integrierten/spezifischen IaCTools ARM und Bicep schreiben. Dann werde ich die „universellen“ Tools Terraform und Pulumi vorstellen.  

In diesem Blogbeitrag werde ich spezifischer auf das Tool „Azure Resource Manager“ (im folgenden ARM genannt) eingehen.

Was ist ARM?

ARM steht für „Azure Resource Manager“ und ist der Bereitstellungs- und Verwaltungsdienst für Azure. Kurz gesagt, ARM ist das in Azure integrierte IaC-Tool. 

In welcher Sprache?

ARM-Vorlagen basieren auf JSON und sind eine deklarative Sprache.

Wie wird das Tool genutzt?

Ein ARM Template ist im Azure-Portal direkt einsetzbar, kann aber auch über Visual Studio Code ausgeführt werden. In unserem Beitrag benutzen wir VS Code, um die Ressourcen zu erzeugen. 

Im Azure Portal

Um den Rahmen des Beitrags nicht zu sprengen, beschränke ich mich auf das für Developer wichtigere Bereitstellungsverfahren über Visual Studio Code, da im Azure Portal keine Versions-Verwaltung der Skripte (etwa mit git) möglich ist. Wer hier mehr wissen möchte, den verweise ich auf die Microsoft Dokumentation.

In Visual Studio Code

Wenn man mit VS Code ein ARM Template erzeugen will, braucht man die Erweiterung:  
„Azure Resource Manager (ARM) Tools“.

Außerdem muss entweder die Azure-Befehlszeilenschnittstelle oder das Azure PowerShell-Modul installiert und authentifiziert sein.

Wenn man dann ein ARM-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 "ResourceGgroupName" --template-file "FileName.json"
				
			

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

				
					az deployment group create -g "ResourceGgroupName" --template-file "FileName.json"
				
			

Optionaler Aufruf mit Parameter-Datei:

				
					az deployment group create -g "ResourceGgroupName" --template-file "FileName.json" --parameters "ParameterFile.json" 
				
			

Wie ist ein ARM-Template aufgebaut?

Durch die Erweiterung „ARM Tools“ erzeugt man die Vorlage für ein ARM-Template, in dem man in VS Code eine JSON Datei erstellt oder öffnet und darin „arm!“ eingibt: 

				
					{  
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",  
"contentVersion": "1.0.0.0",  
"parameters": {},  
"functions": [],  
"variables": {},  
"resources": [],  
"outputs": {}  
}
				
			

Aber was bedeuten die folgenden Punkte eigentlich? In einem kleinen Beispiel erkläre ich sie und gebe noch ein Beispiel mit, indem ich einen Storage Account erzeuge und den Connection String sowie den Storage Account-Namen ausgeben lasse. 

Resources:

Dieser Begriff bezeichnet die Ressourcen, die innerhalb einer Ressourcengruppe oder einer Subscription erzeugt oder geupdatet werden.   

Durch die Extension gibt es viele Ressourcenvorlagen, wie in unserem Beispiel eine Vorlage, um einen Storage Account zu erstellen: 

				
					"resources": [ 
        { 
            "name": "storageaccount1", 
            "type": "Microsoft.Storage/storageAccounts", 
            "apiVersion": "2019-06-01", 
            "tags": { 
                "displayName": "storageaccount1" 
            }, 
            "location": "[resourceGroup().location]", 
            "kind": "StorageV2", 
            "sku": { 
                "name": "Premium_LRS", 
                "tier": "Premium"
            } 
        } 
] 
				
			
Parameters:

Werte die vom Entwickler vorgegeben werden können, um die Bereitstellung anzupassen. Es gibt auch die Möglichkeit eine Parameterdatei zu erstellen, um dort alle Parameter vorzugeben. 

In unserem Beispiel wollen wir nicht immer den gleichen Namen für den Storage Account benutzen. Der Name wird in einem Parameter-File vorgegeben.  

				
					 "parameters": { 
   "storageNamePrefix": { 
     "type": "string", 
     "maxLength": 11 
} 
				
			
Functions:

Es gibt vorgefertigte Funktionen, die man benutzen kann. Hier die vollständige Liste aller verfügbaren Funktionen aus der Dokumentation. Wem das nicht reicht, der kann auch selbst erstellte Funktionen verwenden.

In diesem Beispiel zeige ich eine Funktion, welche eindeutige Namen für Ressourcen zurückgibt. Um genauer zu sein wertet sie den übergebenen Parameter „namePrefix“ aus und gibt die Resource Group ID als Wert zurück. 

				
					"functions": [ 
    "namespace": "contoso", 
    "members": { 
      "uniqueName": { 
        "parameters": [ 
          { 
            "name": "namePrefix", 
            "type": "string" 
          } 
        ], 
        "output": { 
          "type": "string", 
          "value": "[concat(toLower(parameters('namePrefix')), uniqueString(resourceGroup().id))]" 
        } 
      } 
    } 
  } 
] 
				
			
Variables:

Werte, die als JSON-Fragmente in der Vorlage verwendet werden, um Ausdrücke in der Vorlagensprache zu vereinfachen. Variables sollte man am besten nur bei Werten benutzen, die sich wiederholen. 

In diesem Beispiel benutze ich die Location als Variable, da sie für jede Ressource im Template benötigt wird und sich dadurch oft wiederholt. 

Die Variable “RGName” brauche ich für die Ausgabe des Connection Strings. 

				
					"variables": { 
        "location": "[resourceGroup().location]", 
        "RGname" : "[resourceGroup().name]" 
} 
				
			
Outputs:

Nach dem Bereitstellen der Ressourcen kann ich mir bestimmte Werte vom Template ausgeben lassen. In diesem Beispiel lasse ich mir den Storage Account-Namen und den Connection String ausgeben.

				
					"outputs": { 
        "StorageName": { 
            "type": "string",
            "value": "[contoso.uniqueName(parameters('storageNamePrefix'))]" 
        }, 
        "storageAccountConnectionString": {   
            "type": "string",   
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', contoso.uniqueName(parameters('storageNamePrefix')), ';AccountKey=', listKeys(resourceId(variables('RGname'),'Microsoft.Storage/storageAccounts', contoso.uniqueName(parameters('storageNamePrefix'))), '2019-04-01').keys[0].value,';EndpointSuffix=core.windows.net')]"   
        } 
} 
				
			
Finale Version

Final sieht unser komplettes Beispiel wie folgt aus: 

				
					{ 
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 
    "contentVersion": "1.0.0.0", 
    "parameters": { 
   "storageNamePrefix": { 
     "type": "string", 
     "maxLength": 11 
   } 
 }, 
    "functions": [ 
  { 
    "namespace": "contoso", 
    "members": { 
      "uniqueName": { 
        "parameters": [ 
          { 
            "name": "namePrefix", 
            "type": "string" 
          } 
        ], 
        "output": { 
          "type": "string", 
          "value": "[concat(toLower(parameters('namePrefix')), uniqueString(resourceGroup().id))]" 
        } 
      } 
    } 
  } 
], 
    "variables": { 
        "location": "[resourceGroup().location]", 
        "RGname" : "[resourceGroup().name]" 
    }, 
    "resources": [ 
   { 
     "type": "Microsoft.Storage/storageAccounts", 
     "apiVersion": "2019-04-01", 
     "name": "[contoso.uniqueName(parameters('storageNamePrefix'))]", 
     "location": "[variables('location')]",
     "sku": { 
       "name": "Standard_LRS" 
     }, 
     "kind": "StorageV2", 
     "properties": { 
       "supportsHttpsTrafficOnly": true 
     } 
   } 
 ], 
    "outputs": { 
        "StorageName": { 
            "type": "string",
            "value": "[contoso.uniqueName(parameters('storageNamePrefix'))]" 
        }, 
        "storageAccountConnectionString": {   
            "type": "string",   
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', contoso.uniqueName(parameters('storageNamePrefix')), ';AccountKey=', listKeys(resourceId(variables('RGname'),'Microsoft.Storage/storageAccounts', contoso.uniqueName(parameters('storageNamePrefix'))), '2019-04-01').keys[0].value,';EndpointSuffix=core.windows.net')]"   
        } 
    } 
} 
				
			

Erzeugen eines Modern Data Warehouse

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

				
					{ 
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 
    "contentVersion": "1.0.0.0", 
    "parameters": { 
   "storageNamePrefix": { 
     "type": "string", 
     "maxLength": 11, 
     "metadata": { 
        "description": "Specifies the name of the Azure Storage account." 
      } 
   }, 
   "containerName": { 
      "type": "string", 
      "defaultValue": "logs", 
      "metadata": { 
        "description": "Specifies the name of the blob container." 
      } 
    }, 
    "dataFactoryNamePrefix": { 
      "type": "string", 
      "metadata": { 
        "description": "Specifies the name of the Azure Data Factory." 
      } 
    }, 
    "SQLserverNamePrefix": { 
      "type": "string", 
      "metadata": { 
        "description": "Specifies the name of the Azure SQL-Server." 
      } 
    }, 
        "administratorLogin": { 
      "type": "string", 
      "metadata": { 
        "description": "Specifies the Login to SQL-Server." 
      } 
    }, 
    "administratorLoginPassword": { 
      "type": "string", 
      "metadata": { 
        "description": "Specifies the Login Password to SQL-Server." 
      } 
    }, 
    "SQLDBNamePrefix": {
      "type": "string", 
      "metadata": { 
        "description": "Specifies the name of the Azure SQL-DB." 
      } 
    } 
 }, 
    "functions": [ 
  { 
    "namespace": "contoso", 
    "members": { 
      "uniqueName": { 
        "parameters": [ 
          { 
            "name": "namePrefix", 
            "type": "string"
          } 
        ], 
        "output": { 
          "type": "string", 
          "value": "[concat(toLower(parameters('namePrefix')), uniqueString(resourceGroup().id))]" 
        } 
      } 
    } 
  } 
], 
    "variables": { 
        "location": "[resourceGroup().location]", 
        "RGname" : "[resourceGroup().name]" 
    }, 
    "resources": [ 
   { 
      "type": "Microsoft.Storage/storageAccounts", 
      "apiVersion": "2019-06-01", 
      "name": "[contoso.uniqueName(parameters('storageNamePrefix'))]", 
     "location": "[variables('location')]", 
      "sku": {
        "name": "Standard_LRS", 
        "tier": "Standard" 
      }, 
      "kind": "StorageV2", 
      "properties": { 
        "accessTier": "Hot" 
      }, 
      "resources": [ 
        { 
          "type": "blobServices/containers", 
          "apiVersion": "2019-06-01", 
          "name": "[concat('default/', parameters('containerName'))]", 
          "dependsOn": [
            "[contoso.uniqueName(parameters('storageNamePrefix'))]" 
          ] 
        } 
      ] 
    }, 
    {  
      "properties":{},     
      "name": "[contoso.uniqueName(parameters('dataFactoryNamePrefix'))]", 
      "type": "Microsoft.DataFactory/factories", 
      "apiVersion": "2018-06-01", 
      "location": "[variables('location')]", 
      "identity": { 
        "type": "SystemAssigned" 
            } 
        }, 
        { 
       
      "type": "Microsoft.Sql/servers",
      "apiVersion": "2020-02-02-preview",
      "name": "[contoso.uniqueName(parameters('SQLserverNamePrefix'))]", 
      "location": "[variables('location')]", 
      "properties": { 
        "administratorLogin": "[parameters('administratorLogin')]", 
        "administratorLoginPassword": "[parameters('administratorLoginPassword')]" 
      }, 
      "resources": [ 
        {"properties": {}, 
          "type": "databases", 
          "apiVersion": "2020-08-01-preview",
          "name": "[contoso.uniqueName(parameters('SQLDBNamePrefix'))]", 
          "location": "[variables('location')]", 
          "sku": { 
            "name": "Standard", 
            "tier": "Standard" 
          }, 
          "dependsOn": [
            "[resourceId('Microsoft.Sql/servers', concat(parameters('SQLserverNamePrefix')))]" 
          ] 
        } 
      ] 
    }  
  ], 
    "outputs": { 
        "StorageName": {
            "type": "string", 
            "value": "[contoso.uniqueName(parameters('storageNamePrefix'))]" 
        }, 
        "storageAccountConnectionString": {   
            "type": "string",   
            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', contoso.uniqueName(parameters('storageNamePrefix')), ';AccountKey=', listKeys(resourceId(variables('RGname'),'Microsoft.Storage/storageAccounts', contoso.uniqueName(parameters('storageNamePrefix'))), '2019-04-01').keys[0].value,';EndpointSuffix=core.windows.net')]"  
        } 
    } 
} 
				
			

Abschluss

Zum Abschluss dieses Blogbeitrages erläutere ich die Vor- und Nachteile von ARM-Templates: 

 

Zur Entwicklung von ARM-Templates eignet sich Visual Studio Code, da es sehr leichtgewichtig ist und einige Vorteile bietet, wie z.B. integrierte Versionskontrolle. Durch die VS Code Extension für ARM-Templates, die man benötigt, bekommt man Unterstützung wie zum Beispiel IntelliSense. Dies hilft enorm bei der Deklaration von Ressourcen, sowie beim Finden von syntaktischen Fehlern im Template. Viele weitere Vorlagen und mehr Informationen dazu kann man hier finden. 

Zudem unterstützen ARM-Templates den, “Day-Zero”Support, welches viele andere IaC-Tools nicht haben. Das bedeutet, wenn neue Ressourcen oder Features in Azure verfügbar sind, kann man diese durch ARM-Templates sofort bereitstellen, da ARM direkt mit der Azure API spricht. 

Ein weiterer Vorteil, der erwähnenswert ist, hat zwar nicht direkt mit den ARM-Templates selbst zu tun, sondern mit der Azure CLI. Da ARM Templates praktisch nur aus Text bestehen, können Sie vor dem Deployment nur bedingt auf Fehler validiert werden. Dadurch kommt es oft vor, dass Bereitstellungen mit einer Fehlermeldung abbrechen. Mit der Validate-Function in der CLI kann man das Problem umgehen und im Vorfeld in Sekundenschnelle überprüfen, ob ein Template logische Fehler enthält oder nicht, und ob es zu Überschneidungen mit existierenden Ressourcen kommen kann. 

Zuletzt ist es sehr nützlich, dass der ARM-Compiler in der Lage ist, vom Entwickler eingefügte Kommentare zu ignorieren. Dadurch wird der Code deutlich lesbarer. Dies ist standardmäßig bei JSON-files ja bekanntlich nicht möglich. 

 

Trotz der Vorteile gibt es auch einige Probleme bei der Arbeit mit ARM-Vorlagen.  

Je komplexer die Bereitstellungen werden, desto schwieriger wird es, die Vorlagen zu lesen und zu verstehen, da alle Ressourcen geschachtelt dargestellt werden müssen.  Ein Lichtblick hier ist aber zumindest, dass vor einiger Zeit die Möglichkeit geschaffen wurde, mehrere Templates zu linken. Dadurch kann man die Ressourcen zumindest übersichtlich gruppieren in mehreren Files. Mehr dazu hier.

Wie es der Name “Azure” Resource Manager schon sagt, kann man ARM-Vorlagen nur in Azure benutzen. Dieses Tool ist daher also nur für solche Development Teams interessant, die sich auf diesen Cloud Provider festgelegt haben. 

Man kann bereitgestellte Ressourcen nicht über ARM löschen. Es gibt dazu aber einen Trick der in diesem Blog erklärt wird. 

Zuletzt muss auch noch gesagt werden, dass die Einbindung von ARM Templates in übergeordnete Deployment-Prozesse eher schwierig ist. So musste ich für eines meiner Projekte u.a. lokale Dateien in einen Blob-Storage Container hochladen und diese dann auf eine Azure Function deployen. Das war für eines meiner Projekte sehr wichtig und ließ sich nur sehr schwer umsetzen. Dies war letztlich auch der Grund, weshalb ich zu einem anderen Tool gewechselt bin.