Terraform Remote Backend Konfiguration in Microsoft Azure

Joshua Friderici
20. Februar 2024
Lesezeit: 19 min
Terraform Remote Backend Konfiguration in Microsoft Azure

Einleitung

Terraform ist ein Infrastructure-as-Code (IaC) Werkzeug, das vorrangig in DevOps-Teams eingesetzt wird. Durch das von HashiCorp entwickelte Tool können Infrastrukturressourcen in der Cloud oder On-Premises über deklarative Konfigurationsdateien in Form von Code verwaltet werden. Terraform wird verwendet, um eine konsistente und reproduzierbare Bereitstellung von Infrastrukturressourcen zu ermöglichen.

In diesem Artikel erfahren Sie Schritt für Schritt, wie Sie das Terraform Backend in Microsoft Azure initial konfigurieren und es dadurch mit Terraform verwalten können. Dies ist die Basis, um gemeinsam in Teams (z. B. via git) sicher mit Terraform State-Files in der Azure Cloud zu arbeiten. Gleichzeitig dient dies als standardisierter Ausgangspunkt für alle Projekte mit Terraform.

Vorbereitungen

Auf ihrem Arbeitsgerät führen Sie initial die folgenden Installationen durch:

Zusätzlich vergeben Sie im Azure Portal auf die gewünschte Subscription die entsprechenden Berechtigungen, um Ressourcen und Rollen anzulegen (z. B. Owner).

Ordner-/Projektstruktur

Die exakte Struktur von Terraform Projekten kann je nach Projekt, Kunde und/oder persönlichen Vorlieben variieren. Wichtig ist jedoch eine nachvollziehbare und klare Benennung/Sortierung.

Als Basis für dieses Anleitung dient die folgende Struktur:

  • Beispielprojekt
    • Environment
      • Azure
        • terraform
        • deployment
        • modules
        • prerequisite
          • azure_backend

In der Praxis kann das im Zusammenspiel mit GitLab wie folgt aussehen:

Zusammenspiel mit GitLab

Anlegen eines Projekts/Repositories in einem Versionskontrollsystem

Für das Projekt können Sie ein Versionskontrollsystem Ihrer Wahl nutzen (z. B. GitHub, GitLab oder AzureDevOps).

  1. Erstelle Sie im gewünschten Versionskontrollsystem ein neues Projekt bzw. Repository.
  2. Gehen Sie anschließend auf ihrem Arbeitsgerät in VSCode oder IntelliJ in Ihr Root-Projektverzeichnis und verwenden Sie ‘git init’, um das lokale Git Repository zu initialisieren.
  3. Führen Sie anschließend ‘git add .’ aus.
  4. Danach ‘git commit -m”initial commit”‘
  5. Kopieren Sie die URL des gerade erstellten Projekts/Repos in ihrem Versionskontrollsystem.
  6. Fügen Sie die URL in folgenden Befehl ein: ‘git remote add origin <URL>‘ und senden Sie diesen ab.
  7. Abschließend noch ‘git push origin master‘, um Ihr lokales Projekt/Repo in ihr gewünschtes Versionskontrollsystem hochzuladen.

Anlegen der ersten Dateien

Im Verzeichnis azure_backend legen Sie die folgenden Dateien an:

  • data.tf
  • main.tf
  • outputs.tf
  • provider.tf
  • terraform.tfvars
  • variables.tf

Konfiguration des Backends

Wichtig

Für alle Deployments nach Terraform Best Practices gilt:
Alle Variablen werden in der variables.tf definiert. Die terraform.tfvars wiederum dient der zentralen Pflege der Werte.
Dies sorgt für eine bessere Übersicht und erleichtert Anpassungen.

data.tf

  • Datenquellen ermöglichen es Terraform, Informationen zu nutzen, die außerhalb von Terraform definiert wurden, die durch eine andere separate Terraform-Konfiguration definiert wurden oder die durch Funktionen verändert wurden. (Quelle)
  • Die data.tf enthält den folgenden Code:
data "azurerm_subscription" "primary" {
}
  • Diese Ressource wird dafür verwendet, um uns Informationen über die Subscription auszugeben, in der das Backend deployed wird.
  • Wir greifen diese in der outputs.tf auf.

main.tf

  • Die main.tf enthält das eigentliche Deployment
resource "azurerm_resource_group" "main" {
  name     = var.rgname_tfstate
  location = var.location
  tags     = merge(var.default_tags)
}

resource "random_string" "main" {
  length  = 4
  upper   = false
  special = false
}

resource "azurerm_storage_account" "backend" {
  name                            = "satfstate${random_string.main.result}"
  resource_group_name             = azurerm_resource_group.main.name
  location                        = azurerm_resource_group.main.location
  account_tier                    = var.account_tier
  account_kind                    = var.account_kind
  access_tier                     = var.access_tier
  account_replication_type        = var.account_replication_type
  min_tls_version                 = var.min_tls_version
  enable_https_traffic_only       = var.enable_https_traffic_only
  allow_nested_items_to_be_public = var.allow_nested_items_to_be_public

  blob_properties {
    last_access_time_enabled = var.last_access_time_enabled
  }

  network_rules {
    default_action = var.default_action
    bypass         = var.bypass
    ip_rules       = var.ip_rules
  }
  lifecycle {
    ignore_changes = [
      network_rules,
    ]
  }

  tags = merge(var.default_tags)
}

resource "azurerm_storage_container" "tfstate" {
  for_each              = var.prefix_environment
  name                  = "tfstate-${each.value}"
  storage_account_name  = azurerm_storage_account.backend.name
  container_access_type = var.container_access_type
}

resource "azurerm_management_lock" "lock_storage_account_tfstate" {
  name       = "lock-${azurerm_storage_account.backend.name}"
  scope      = azurerm_storage_account.backend.id
  lock_level = "CanNotDelete"
  notes      = "Locked because it's needed to store the tfstate file"
  depends_on = [azurerm_storage_container.tfstate]
}
  1. Zu Beginn definieren wir eine Resource Group.
  2. Anschließend generieren wir einen Random String.
  3. Dieser Random String wird bei der darauffolgenden Erstellung des Storage Accounts verwendet. Er dient als Zusatz für den Namen, da dieser in Azure global einzigartig sein muss.
  4. Im nächsten Schritt wird der Storage Account definiert, der die Container für die Speicherung von Terraform State-Files beherbergt.
    • Definiert werden vor allem sicherheitsrelevante Aspekte, wie z. B. der Zugriff auf den Storage Account mittels dem network_rules Block.
    • Der lifecycle Block sorgt mit ignore_changes dafür, dass eine manuelle Anpassung der IP-Addressen oder Ranges im Azure Portal, von Terraform ignoriert wird.
    • Weitere Informationen zum storage_account findet sich in den Beschreibungen der variables.tf und in der offiziellen Terraform Dokumentation
  5. Im Anschluss folgt die Erstellung der Container innerhalb des zuvor erstellten Storage Accounts.
    • Die entsprechenden Container werden mittels for_each und einem Mapping in der terraform.tfvars definiert
  6. Abschließend wird ein Resource Lock erstellt, um ein versehentliches Löschen des Storage Accounts im Portal zu verhindern.

outputs.tf

  • Output-Werte machen Informationen über Ihre Infrastruktur in der Kommandozeile verfügbar und können Informationen für andere Terraform-Konfigurationen zur Verfügung stellen. Output-Werte sind vergleichbar mit Rückgabewerten in Programmiersprachen. (Quelle)
  • Inhalt:
output "primary_subscription" {
  description = "Displays the current subscription"
  value       = data.azurerm_subscription.primary.display_name
}

output "backend_storage_account" {
  description = "Displays the name of the current storage account used as a backend"
  value       = azurerm_storage_account.backend.name
}

output "backend_resource_group" {
  description = "Displays the name of the resource group, where the backend is located"
  value       = azurerm_resource_group.main.name
}

output "backend_storage_container_backend" {
  description = "Displays the name of the storage container, where the tfstate is located"
  value       = azurerm_storage_container.tfstate["backend"].name
}

output "backend_storage_container_dev" {
  description = "Displays the name of the storage container, where the tfstate is located"
  value       = azurerm_storage_container.tfstate["development"].name
}

output "backend_storage_container_prod" {
  description = "Displays the name of the storage container, where the tfstate is located"
  value       = azurerm_storage_container.tfstate["production"].name
}

output "managed_by_terraform" {
  description = "Displays the name of the tag, which is used to identify resources managed by terraform"
  value       = var.default_tags["managed_by_terraform"]
}

output "created_by" {
  description = "Displays the name of the tag, which is used to identify the creator of a resource"
  value       = var.default_tags["created_by"]
}

output "responsibility" {
  description = "Displays the name of the tag, which is used to identify the responsibility of a resource"
  value       = var.default_tags["responsibility"]
}

Der Output teilt sich in zwei Bereiche:

  • Alle Outputs mit dem Zusatz ‘.name’ geben den Namen der definierten Ressource in der Kommandozeile aus.
    Dies dient primär der Übersicht, nachdem die Ressourcen deployed wurden.
  • Die letzten drei Outputs werden für das zentrale Tagging im Projekt verwendet.
    Grundsätzlich besitzt jedes Projekt Angaben (z. B. Kostenstelle oder wer und womit die Ressource erstellt wurde), die für alle Ressourcen gelten. Aus diesem Grund werden diese Tags im Backend definiert und ausgegeben. Mehr dazu am Ende dieses Projektes.

provider.tf

  • Terraform verlässt sich auf Plugins, die Provider genannt werden, um mit Cloud-Anbietern, SaaS-Anbietern und anderen APIs zu interagieren. Terraform-Konfigurationen müssen angeben, welche Provider sie benötigen, damit Terraform sie installieren und verwenden kann. Zusätzlich benötigen einige Provider eine Konfiguration (wie Endpunkt-URLs oder Cloud-Regionen), bevor sie verwendet werden können. (Quelle)
  • Inhalt:
terraform {
  required_version = ">=1.6.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~>3.80"
    }
    random = {
      source  = "hashicorp/random"
      version = "~>3.6.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}
}

terraform.tfvars

  • Um viele Variablen zu verwalten, ist es praktischer, ihre Werte in einer Variablendefinitionsdatei zu definieren.
  • Inhalt:
# Project Constants
location = "West Europe"

prefix_environment = {
  backend     = "backend"
  development = "dev"
  production  = "prod"
}

default_tags = {
  managed_by_terraform = "True"
  created_by           = "Joshua Friderici"
  responsibility       = "Ansgar Dahlen"
}

rgname_tfstate = "rg-terraform-state"

# Storage Account
account_tier                    = "Standard"
account_kind                    = "StorageV2"
access_tier                     = "Cool"
account_replication_type        = "RAGRS"
min_tls_version                 = "TLS1_2"
enable_https_traffic_only       = true
allow_nested_items_to_be_public = false
last_access_time_enabled        = true
default_action                  = "Deny"
bypass                          = ["Logging", "Metrics", "AzureServices"]
ip_rules                        = ["127.168.0.1"] # TODO: Edit the ip ranges/addresses to get access to the state file

# Container
container_access_type = "private"
  • Passen Sie die ip_rules an und tragen Sie hier Ihre aktuelle IP-Adresse oder IP-Range ein, von der aus Sie mit dem Storage Account kommunizieren möchten
  • Angaben unter default_tags dürfen Sie auch gerne an Ihre Person/Umgebung anpassen
  • Die Werte unter prefix_environment können Sie jederzeit erweitern, falls Sie bspw. weitere Container deployen möchten

variables.tf

  • Enthält alle für das Deployment relevanten Variablen
# Project Constants
variable "location" {
  description = "(Required) The location where the resource should be created."
  type        = string
}
variable "prefix_environment" {
  description = "Used as prefix for resources: Defines the staging environment, where the Azure resources are getting deployed. It can either be dev (development) or prod (production)."
  type        = any
}
variable "default_tags" {
  description = "Map of default tags, used as standard for the whole project."
  type        = any
}
variable "rgname_tfstate" {
  description = "(Required) The Name which should be used for the Resource Group. Changing this forces a new Resource Group to be created."
  type        = string
}

# Storage Account
variable "account_tier" {
  description = "(Required) Defines the Tier to use for this storage account. Valid options are Standard and Premium. For BlockBlobStorage and FileStorage accounts only Premium is valid. Changing this forces a new resource to be created."
  type        = string

  validation {
    condition     = contains(["Standard", "Premium"], var.account_tier)
    error_message = "The value of the account_tier property is invalid. Only Standard and Premium are allowed"
  }
}

variable "account_kind" {
  description = "(Optional) Defines the Kind of account. Valid options are BlobStorage, BlockBlobStorage, FileStorage, Storage and StorageV2. Defaults to StorageV2."
  type        = string

  validation {
    condition     = contains(["BlobStorage", "BlockBlobStorage", "FileStorage", "StorageV2"], var.account_kind)
    error_message = "The value of the account_kind property is invalid. Only BlobStorage, BlockBlobStorage, FileStorage and StorageV2 are allowed"
  }
}

variable "access_tier" {
  description = "(Optional) Defines the access tier for BlobStorage, FileStorage and StorageV2 accounts. Valid options are Hot and Cool, defaults to Hot."
  type        = string

  validation {
    condition     = contains(["Hot", "Cool"], var.access_tier)
    error_message = "The value of the access_tier property is invalid. Only Hot and Cool are allowed"
  }
}

variable "account_replication_type" {
  description = "(Required) Defines the type of replication to use for this storage account. Valid options are LRS, GRS, RAGRS, ZRS, GZRS and RAGZRS."
  type        = string

  validation {
    condition     = contains(["LRS", "GRS", "RAGRS", "ZRS", "GZRS", "RAGZRS"], var.account_replication_type)
    error_message = "The value of the account_replication_type property is invalid. Only LRS, GRS, RAGRS, ZRS, GZRS and RAGZRS are allowed"
  }
}

variable "min_tls_version" {
  description = "(Optional) The minimum supported TLS version for the storage account. Possible values are TLS1_0, TLS1_1, and TLS1_2. Defaults to TLS1_2 for new storage accounts."
  type        = string

  validation {
    condition     = contains(["TLS1_2"], var.min_tls_version)
    error_message = "The value of the account_replication_type property is invalid. Only TLS1_2 is allowed, because versions 1.0 and 1.1 are no longer supported by Microsoft."
  }
}

variable "enable_https_traffic_only" {
  description = "(Optional) Boolean flag which forces HTTPS if enabled, see here for more information. Defaults to true."
  type        = bool
}

variable "allow_nested_items_to_be_public" {
  description = "(Optional) Allow or disallow nested items within this Account to opt into being public. Defaults to true."
  type        = bool
}

variable "last_access_time_enabled" {
  description = "(Optional) Is the last access time based tracking enabled? Default to false."
  type        = bool
}

variable "default_action" {
  description = "(Required) Specifies the default action of allow or deny when no other rules match. Valid options are Deny or Allow."
  type        = string

  validation {
    condition     = contains(["Deny"], var.default_action)
    error_message = "The value of the default_action property is invalid. Only Deny is allowed."
  }
}

variable "bypass" {
  description = "(Optional) Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Valid options are any combination of Logging, Metrics, AzureServices, or None."
  type        = list(string)

  validation {
    condition     = length([for b in var.bypass : b if b != "None"]) <= length(["Logging", "Metrics", "AzureServices"])
    error_message = "The value of the bypass property is invalid. Only Logging, Metrics, AzureServices, or None are allowed."
  }
}

variable "ip_rules" {
  description = "(Optional) List of public IP or IP ranges in CIDR Format. Only IPv4 addresses are allowed. /31 CIDRs, /32 CIDRs, and Private IP address ranges (as defined in RFC 1918), are not allowed."
  type        = list(string)
}

# Container
variable "container_access_type" {
  description = "(Optional) The access type for the container. Possible values are blob, container or private. Defaults to private."
  type        = string

  validation {
    condition     = contains(["private"], var.container_access_type)
    error_message = "The value of the container_access_type property is invalid. Only private is allowed."
  }
}

Beachten Sie hier vor allem die validation Blöcke, die Teil von einigen Variablen sind. Diese Blöcke erlauben es die Regeln für die Vergabe von Werten festzulegen. Zum Beispiel legt die validation der Variable min_tls_version fest, dass nur die aktuelle Version 1.2 verwendet werden darf, da Microsoft ältere Versionen nicht länger unterstützt.

Initiales Deployment

  1. Loggen Sie sich mithilfe der Azure CLI in ihrem Account ein und wähle die entsprechende Subscription aus, in der das Deployment des Backends erfolgen soll.
    • az login
    • az account list (Ruft eine Liste der Subscriptions für das angemeldete Konto ab)
    • az account show (Ruft Details zur aktuell ausgewählten Subscription ab)
    • az account set –subscription “SUBSCRIPTION_ID“ (Wählt eine Subscription aus)
  2. Begeben Sie sich in der Kommandozeile in den Ordner azure_backend
  3. Führen Sie anschließend die folgenden Befehle in der Kommandozeile aus:
    • terraform init (Initialisiert das Arbeitsverzeichnis mit den Terraform Konfigurationsdateien)
    • terraform plan (Erstellt einen Ausführungsplan. Prüfen Sie diesen genau, bevor Sie den nächsten Schritt durchführen)
    • terraform apply -auto-approve (Führt die im Ausführungsplan aufgeführten Aktionen aus. -auto-approve verhindert, dass nach einer Bestätigung gefragt wird)
    • Tipp für Schreibfaule:
    • Definieren Sie zu Beginn einen Alias, z. B. alias tf=terraform. Damit verkürzen sich die oben genannten Befehle.
  4. Sobald die Ressourcen erfolgreich deployed wurden, werden Sie im azure_backend Ordner die terraform.tfstate Datei finden.
  5. Aktuell haben wir kein Backend für unser Backend definiert, weshalb die terraform.tfstate Datei lokal gespeichert wird.
  6. Hierbei handelt es sich um das typische “Henne – Ei” Problem, das wir in Azure im nächsten Schritt lösen werden.

Übertragung und Verwaltung des Terraform Backends in Azure

Um das bereits erwähnte “Henne – Ei” Problem in Azure zu lösen und den State des Backends zentral und sicher zu speichern, bedarf es folgender Schritte:

  1. Im Verzeichnis azure_backend wird sich nach dem initialen Deployment die Datei terraform.tfstate befinden.
    Benennen Sie diese Datei in backend.tfstate um.
  2. Loggen Sie sich in das Azure Portal ein und wählen Sie den Storage Account aus, den Sie zuvor mit Hilfe von Terraform deployed haben.
  3. Anschließend wählen Sie den Container tfstate-backend aus und laden die backend.tfstate Datei hoch.
  4. Jetzt erstellen Sie in ihrem Ordner azure_backend eine neue Datei mit dem Namen backend.tf mit folgendem Inhalt
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "satfstates8g8"
    container_name       = "tfstate-backend"
    key                  = "backend.tfstate"
  }
}
  1. Bitte passen Sie die Werte (z. B. storage_account_name) entsprechend Ihrer Konfiguration an.
  2. Die im Ordner azure_backend befindlichen .tfstate Dateien und den Ordner .terraform können Sie jetzt löschen.
  3. Führen Sie den folgenden Befehl aus, um das backend erfolgreich zu initialisieren terraform init -migrate-state
  4. Mit terraform plan können Sie sichergehen, dass alles erfolgreich geklappt hat. Die Ausgabe sollte wie folgt aussehen:
Ausgabe terraform plan

Herzlichen Glückwunsch!
Damit haben Sie Ihr standardisiertes und durch Terraform gemanagtes Backend in Azure bereitgestellt.

Hinweis:
Sollten Sie Ihre aktuelle IP-Adresse in der terraform.tfvars Datei angegeben haben, dann müssen Sie diese regelmäßig im Portal → Storage Account → Networking aktualisieren bzw. die jeweils aktuelle IP-Adresse hinterlegen. Ansonsten haben Sie keinen Zugriff auf die Terraform State Dateien.

Alternative initiale Bereitstellung:
Bekanntlich führen viele Wege nach Rom. Es ist Ihnen möglich den Storage Account für das Terraform Backend mittels PowerShell oder Azure CLI bereitzustellen. Anschließend können Sie durch Zuhilfenahme des Terraform Import Block, den bestehenden Storage Account importieren. Im Gegensatz zum Terraform-Import-Befehl ist der konfigurationsgesteuerte Import mit Import-Blöcken zuverlässig, funktioniert mit CICD-Pipelines und ermöglicht Ihnen eine Vorschau des Importvorgangs, bevor Sie den State ändern.

Einmal importiert, verwaltet Terraform die Ressource mithilfe Ihrer State-File. Sie können dann die importierte Ressource wie jede andere verwalten, ihre Attribute aktualisieren und sie als Teil eines Standard-Ressourcenlebenszyklus entfernen.

Bonus: Zusätzliche Information im Umgang mit Tags

Die Frage nach effektiven Tagging-Methoden stellt sich, insbesondere in großen Projekten, immer wieder.
Hiermit stelle ich eine Möglichkeit vor, gängige Tags zentral im Backend zu verwalten und an weitere Module im Projekt zu übergeben.

Drei Dateien im azure_backend Modul stehen hierbei im Fokus:

variables.tf – Definition der Variable

variable "default_tags" {
  description = "Map of default tags, used as standard for the whole project."
  type        = any
}

terraform.tfvars – Definition der Werte/der Tags mittels Map

default_tags = {
  managed_by_terraform = "True"
  created_by           = "Joshua Friderici"
  responsibility       = "Ansgar Dahlen"
}

outputs.tf – Ausgabe der Tags, damit diese von weiteren Modulen im Projekt genutzt werden können.

output "managed_by_terraform" {
  description = "Displays the name of the tag, which is used to identify resources managed by terraform"
  value       = var.default_tags["managed_by_terraform"]
}

output "created_by" {
  description = "Displays the name of the tag, which is used to identify the creator of a resource"
  value       = var.default_tags["created_by"]
}

output "responsibility" {
  description = "Displays the name of the tag, which is used to identify the responsibility of a resource"
  value       = var.default_tags["responsibility"]
}

Wie kann mein Modul diese Tags jetzt nutzen?
Nehmen wir an, ich habe ein Root Modul für eine Entwicklungsumgebung, mit dem ich Azure Virtual Desktop Ressourcen deployen möchte.
Das Verzeichnis und dessen Inhalt könnte bspw. wie folgt aussehen:

Verzeichnis

Damit wir die Tags des Backends verwenden können, verwende ich Terraform data und locals in meinem Modul.
In der data.tf Datei, definiere ich meinen terraform state, um anschließend auf die Outputs des Backends zugreifen zu können:

# Remote state lookup for managing common resource tags
data "terraform_remote_state" "backend" {
  backend = "azurerm"
  config = {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "satfstates8g8"
    container_name       = "tfstate-backend"
    key                  = "backend.tfstate"
  }
}

In der locals.tf Datei, definiere ich meine default_tags

locals {
  default_tags = {
    managed_by_terraform = data.terraform_remote_state.backend.outputs.managed_by_terraform
    created_by           = data.terraform_remote_state.backend.outputs.created_by
    responsibility       = data.terraform_remote_state.backend.outputs.responsibility
    environment          = var.environment
  }
}

In den variables.tf Dateien meiner einzelnen Module definiere ich ebenfalls default_tags:

variable "default_tags" {
  description = "Map of default tags, used as standard for the whole project."
  type        = any
}

Die locals und entsprechenden Tags übergebe ich anschließend im Root Modul meines Deployments, siehe default_tags = local.default.tags

Die locals und entsprechenden Tags übergebe ich anschließend im Root Modul meines Deployments, siehe default_tags = local.default.tags

Der Vorteil dieser Methode besteht nicht nur darin, dass ich gängige Tags im gesamten Projekt zentral im Backend verwalten kann.
Es ist mir darüber hinaus möglich in der locals.tf des jeweiligen Root Moduls unter default_tags, weitere Tags zu ergänzen, die bspw. speziell auf die Entwicklungsumgebung ausgelegt sind. Hier in dem Beispiel ist es die Variable environment, hinter der sich der Wert development verbirgt. In der Praxis wären jedoch auch weitere umgebungsspezifische Tags möglich, wie z. B. die Kostenstelle.

Im Portal sieht das Ergebnis wie folgt aus:

Backend
Backend
Beispiel Ressource in einer Entwicklungsumgebung
Beispiel Ressource in einer Entwicklungsumgebung

Fazit

Die in diesem Blogartikel aufgezeigte Konfiguration des Terraform Backends in Azure stellt die Grundlage für ein kollaboratives Arbeiten mit Terraform als IaC-Tool in Azure dar. Durch die detaillierte Schritt-für-Schritt-Anleitung mit eingearbeiteten Best Practices haben Sie nach dem Durchführen der Konfiguration Ihre Basis für erfolgreiche Terraform-Projekte in Azure geschaffen und sind startklar für Ihr Projekt.