Azure Infrastruktur erstellen mit Terraform



Einleitung

Heute habe ich mich intensiv mit Terraform beschäftigt, einer Provisioner Lösung für Infrastruktur in einer Cloud-Umgebung. Das Fazit ist mehrheitlich positiv, obschon Terraform in der heutigen Version noch einige Mängel aufweist. Trotzdem wird ein DevOps Ingenieur nicht darum herum kommen, sich mit diesem Tool auseinanderzusetzen. Gerade die Informationen die Hashicorp für den nächsten Majorrelease durchsickern lässt, sind vielversprechend.

Infrastructure as Code (IaC) und Configuration as Code (CaC) sind relativ junge Disziplinen im Bereich DevOps, die sich mit der programmatischen Bereitstellung von IT Infrastrukturen beschäftigen. In einer modernen Cloud Umgebung wie AWS, Google Cloud oder Microsoft Azure gibt es heute die Möglichkeit, eigene Infrastrukturen auf rasante Art und Weise zusammenzustellen. Dies geschieht einerseits mit einem Web-GUI, wo die Komponenten definiert und "verdrahtet" werden und andererseits über Automatisierung der manuellen Schritte, die im Web-GUI noch vonnöten sind.

Die Automatisierung hat viele Vorteile gegenüber der manuellen Erstellung von Infrastruktur. Nach einem nicht zu unterschätzenden Initial-Aufwand, kann in mehrer Hinsicht von einer Automatisierungslösung profitiert werden. Anstatt nun einige Vorteile zu nennen, denken Sie sich einen Vorteil und schauen Sie, ob sie den Vorteil in einer der folgenden Kategorien einordnen können: Self-Service, Speed and Safety, Documentation, Version Control, Validation, Reuse und Happiness (betrifft das nur den Kunden?).

Gerade Personen, die ein hohes technisches Verständnis für Netzwerk-, Computer- und Speicherressourcen haben werden in naher Zukunft nicht darum herum kommen, sich mit IaC und CaC auseinanderzusetzen.

Terraform ist in erster Linie ein Provisioning Tool

Ein Provisioning Tool befasst sich grundsätzlich mit dem Aufbau der Infrastruktur und reibt sich beim Aufbau der Computing Ressourcen ein bisschen mit dem Templating. Das heisst die Disziplinen vermischen sich in diesem Grenzbereich. Terraform ist aber ganz sicher ein Provisioning Tool. Templating Tools sind mehr dazu da, vordefinierte "Schablonen" von Installationen auf verschiedene Hosts zu provisionieren.

Terraform ist keine vollständige Programmiersprache


Eine vollständige Programmiersprache ist Terraform sicher nicht. Es stehen nur beschränkte Mittel zur Verfügung, Einfluss auf den Ablauf eines Terraform Programms zu nehmen. Schleifen und IF-THEN-ELSE Konstrukte stehen nur beschränkt zur Verfügung. Ausgangspunkte jedes Terraform Codes sind Schablonen, die mit dynamischen Variablen konfiguriert werden können. Diese Schablonen sollen die API abdecken die der Cloud Provider dem Programmierer zur Verfügung stellt. Zusätzliche Programme sind ad hoc nicht notwendig, da Terraform Code direkt mit dieser API interagiert. Speziellere Aufgaben können über das Templating von Code auf der Konsole gelöst werden.   

Elemente von Terraform


Im Zentrum von Terraform steht das Template. Es gibt sehr viele davon. Jedes Template ist dazu konzipiert in Azure entweder ein Element der Infrastruktur zu erzeugen oder aber ein bestehendes Element der Infrastruktur abzufragen. Diejenigen Templates die Erzeugen werden auch Ressourcen genannt. Eine Ressource wird durch ihren Zweck, einem Namen und einem Block aus Variablen definiert. Im folgenden Beispiel sehen wird eine Ressource für die Erstellung eines virtuellen Netzwerkes:



resource "azurerm_virtual_network" "name" { 
   name = "vnname"    
   address_space = ["10.0.0.0/16"]    
   location = "westeuropa"   
   resource_group_name = "ressourcegroupname" 
}


Der Zweck der Ressource tönt an, dass dieses Template für Microsoft Azure gültig ist. Der Name ist ein hart-codierter String. Hier "name". Im Block darunter sind die dynamischen Elemente auszumachen. Die linke Seite eines Blockelements enthält den Parameter, dem dynamische Werte zugewiesen werden können. Im Beispiel wurden aber hart-codierte Strings gewählt.

Im nächsten Beispiel sehen wir ein Daten-Template. Datentemplates werden verwendet, um Daten der Infrastruktur abzufragen:


    data "azurerm_managed_disk" "info" {
    name = "vorhandenerDiskName"
    resource_group_name = "inderGruppeeingeordnet"
}


Solch ein Datenelement liefert unter anderen Daten meistens eine ID zurück. Die IDs werden dann gebraucht, um Infrastruktur Elemente miteinander zu verknüpfen. Wir sehen dies am Beispiel eine definierten Inline-Blocks für die Erzeugung einer virtuellen Maschine:

storage_data_disk {
    name = "${data.azurerm_managed_disk.info}"
    managed_disk_id = "${data.azurerm_managed_disk.info.id}"
    create_option = "Attach"
    lun = 1
    disk_size_gb = "20"
}

Dabei verwenden wir jetzt schon das Konzept der Variable. Variablen werden gebraucht, um Parametern dynamisch Werte zuzuweisen. Im obigen Beispiel haben wir ein Datenelement gebraucht, um Daten über eine bestehende Disk via API anzufordern. Diese Infos werden wir dann in einer Ressource, hier in einem sogenannten Inline-Block der Ressource für die Erzeugung von virtuellen Maschinen, übergeben.

Variablen


Eine Variable enthält einen dynamischen Wert, der im Code der Variable fest zugewiesen werden kann, oder aber von einem Daten- oder Ressourcenelement bereitgestellt wird.

Die Wert-Abfrage geschieht über folgendes Konstrukt:

"${variablename}"

Daten und Ressourcen bilden Blöcke. Um auf Werte dieser Werte zugreifen zu können, muss folgende Konvention beachten werden:

"${data.Template-Identifikation.Name.OutputVariable}"

Zusammengefasst kann gesagt werden, dass Daten- und Ressourcenelemente Werte erzeugen und diese in Variable abgespeichert werden. Auf diese Werte kann mit Angabe eines Identifiers zugegriffen werden.

Von der Struktur her, wird in den allermeisten Fällen der Code für eine spezifische Aufgabe in einem eigenen Verzeichnis abgelegt. In diesem Verzeichnis befinden sich drei Dateien:


----- Verzeichnis
       ... main.tf
       ... variables.tf
       ... outputs.tf


In der Datei main.tf werden die Ressourcen- und Datenblöcke definiert. Damit Terraform sie initialisieren kann, müssen den Variablen Werte zugeordnet sein. Die Definition der Variablen erfolgt in der Datei variables.tf. Andererseits generieren Ressource- und Datenblöcke Werte. Diese werden wiederum auch in Variablen geschrieben, die in der Datei outputs.tf definiert sind:


[main.tf]

data "azurerm_virtual_machine" "test" {
   name = "production"
   resource_group_name = "${var.resource_group_name}"
}

[variables.tf]

variable "resource_group_name" {
   type = "string"
}

[outputs.tf]

output "virtual_machine_id" {
   value = "${data.azurerm_virtual_machine.test.id}"
}

Mit dem Schlüsselwort "var" wird auf eine Variable in der Datei variables.tf zugegriffen, wie im obigen Beispiel ersichtlich ist. Ausgaben, werden nicht in der eigenen main.tf verwendet. Andererseits kann aber die Datei main.tf durchaus mehrere Ressourcen-/Datentemplates enthalten, die voneinander abhängig sind. Der Trick dabei ist, wie in der Datei outputs.tf angegeben, direkt den Identifier zu verwenden, hier data.azurerm_virtual_machine.test.id, und dann diesen dem anderen Ressourcen-/Datentemplate zuzuweisen.

Wir wollen die Diskussion über Variablen später fortsetzen und uns über Module und den allgemeinen Aufbau der Datei main.tf unterhalten:

Module


Ein spezifisches Verzeichnis mit mindestens einer ausführbaren Terraform Datei (beispielsweise main.tf) und optional einer Variablendatei und einer Outputsdatei ist ein Modul.

Beispiel:


--- module
    ---- module1
         ...main.tf
         ...variables.tf
         ...outputs.tf


Module können in anderen Modulen referenziert werden durch das Schlüsselwort module


[main.tf]

module "create-a-vm" {
   source = "../../module1"

   [Variablen referenziert in Modul 1]
    vm_name = "Meine VM"
}

[../../module1/variables.tf]

variable "vm_name" {
    type = "string"
}

[../../module1/main.tf]

resource "azurerm_virtual_machine" "vm" {
    name = "${var.vm_name}"
...
}

Schleifen-Loops


Schleifen sind nicht direkt programmierbar in Terraform. Hingegen kann angegeben werden, dass eine Ressource mehrmals hintereinander ausgeführt werden soll. Dazu wird der reservierten Variable count einen Wert zugeordnet:



[main.tf]
resource "azurerm_virtual_machine" "vm" {
    
    count = 5
    name = "${var.vm_names[count.index]}"
...
}

[outputs.tf]
output "ids" {
   value = "${data.azurerm_virtual_machine.vm.*.id}"
}

Das gleiche gilt für Datenblöcke unter Angabe von count.

Die Ausgabe in outputs.tf im Modul kann über


locals {
   vm_ids = ["${module.create-multiple-vms.ids}"]
}

module "another-module" {
   source = "../../modulex" 
   module_var = "${local.vm_ids}"
}

 

Listen und Maps


Listen und Maps sind auch möglich zu definieren. 

locals {

  list_1 = "${list("entry1","entry2","entry3")}"
  list_2 = ["entry1", "entry2", "entry3"]
}


In der ersten Verwendung wird Gebrauch von der sogenannten Interpolations-Syntax gemacht. Die zweite Definition verwendet die übliche Notation von eckigen Klammern. Listen können, wenn sie als Variablen verwendet werden sollen, einen Default-Wert enthalten:

variable "mylist" {
  type = "list"

  default = [
     "entry1",
     "entry2",
     "entry3"
  ]
}

Schauen wir noch die Maps an. Maps sind Schlüssel-, Wertepaare

variable "mymap" {
   type = "map"

   default = {
      "key1" = "value1"
      "key2" = "value2"
   }
}

Analog mit der Interpolationssyntax:

locals {
  map_1 = "${map ("key1","value1","key2", "value2")}"
  map_2 = "{
      "key1" = "value1"
      "key2" = "value2" 
  }"
}

Kombinationen von Listen und Maps sind auch möglich. Üblich ist es eine Liste von Maps zu definieren:

Zuweisung:


locals {
  mapslist_1 = "${list (map ("key1","value1","key2", "value2"), map ("key3", "value3", "key4", "value4"))}"
  mapslist 2 = [
      { 
         "key1" = "value1" 
         "key2" = "value2"
      },
      {
         "key3" = "value3"
         "key4" = "value4"
      
      }       
  ]   
}

Abfrage:

Die Abfrage erfolgt folgendermassen:


   Auf eine Map zugreifen: 
      "${local.mapslist_1[index]}"
   Auf ein Element in der Map zugreifen:
      "${lookup(local.mapslist_1[index], "key12")}

   alternativ:
       "${lookup(element (local.mapslist_1,index), "key1")}
   eine Liste ausfüllen:

        ... securitygroup_ids = ["${local.securitygroupsids.*.id}"]    


 

Bedingungen


Eine IF-THEN-ELSE Struktur existiert in Terraform nicht. Jedoch eine Form Boolscher Ausdrücke.
IF wird folgendermassen realisiert: Die Ressourcen enthalten ja das Schlüsselwort count, das angibt wie oft eine Ressource erzeugt werden soll. Damit kann mit "IF Bedingung, dann erzeuge Ressource" formuliert werden. Im Modul kann dann relativ einfach ein IF-THEN-ELSE Konstrukt aufgebaut werden.



resource "azurerm_virtual_machine" "vm_windows" {
  count = "${var.vm_windowstype ? "1" : "0"}"
  
  ...
  os_profile_windows_config {
     ...  
  }
}

resource "azurerm_virtual_machine" "vm_linux" {
  count = "${var.vm_windowstype ? "0" : "1"}"

  ...

} 

Im obigen Beispiel wird je nach Wert der Variablen vm_windowstype eine virtuelle Maschine für Linux oder eine für Windows erstellt.

Spezielles


Projektumgebung einrichten


Damit mit Terraform optimal gearbeitet werden kann, lohnt es sich eine Projektstruktur aufzubauen. Im Prinzip existieren zwei Rollen von Personen, die mit Terraform Code arbeiten. Einerseits ist die Rolle des Code-Entwickler zu nennen und andererseits die Rolle des Infrastrukturentwicklers. Der Infrastrukturentwickler wird den Code vom Code-Entwickler verwenden, ohne aber direkt zu codieren. Er definiert beispielsweise, was für Subnets generiert werden sollen oder welche virtuelle Maschinen definiert werden sollen. Er beschreibt diese Konfiguration in Form von Variablen, die dann von entsprechenden Modulen des Codeentwicklers als Argumente für die Verarbeitung mit Terraform genommen werden.

In der Praxis lohnt es sich ein Verzeichnis live zu erstellen, das als Unterverzeichnis für alle vom Infrastrukturentwickler zu verwendenden Variablen dient:


LIVE
   environment.tfvars
   --- STAGING
       ---- GLOBALS
            globals.tfvars
       ---- MARIO
            ------MYPROJECT
                      vars.tfvars
       ---- ANNA
            ------OTHERPROJECT
                      vars.tfvars
       
   --- PROD
       ---- GLOBALS
            globals.tfvars
       ---- PROJECT_A
               vars.tfvars
       ---- PROJECT_B
               vars.tfvars

Die einzigen Dateien, die angepasst werden müssen, um ein Deployment zu starten sind environment.tfvars und globals.tfvars

[environment.tfvars]
   environment = "staging"   (oder "prod")

[global.tfvars - in staging]
   project = "myproject"
   user    = "Anna"
[global.tfvars - in prod]
   project = "prodproject"
 
Mit mehr Code wird der Infrastrukturentwickler nicht konfrontiert.

Eine feingranulare Unterteilung kann noch nach Verwendungszweck realisiert werden. Also beispielsweise ein Verzeichnis:
/live/staging/mario/project/create-virtual-machines/
und dann dort eine Datei vars.tfvars definieren.

Der Code-Entwickler andererseits hat die Aufgabe, das die mit Werten versehenen Variablen in seinen Code kommen. Er arbeitet auf einer anderen Verzeichnisstruktur, muss aber in seinem Code schauen, dass er die Werte importieren kann.


LIVE
   ...

PROD
   --- MODULES
       --- PROJECT
              ---- MODULE1
              ---- MODULE2
       --- MODULE3
       --- MODULE4
           --- SUBMODULE1
               ---SUBSUBMODULE1
                       main.tf
                       variables.tf
                       outputs.tf

STAGING
   [analog]

DEV
   [analog]

Hier sind die für eine Softwareentwicklung üblichen drei Zweige Production/Staging und Development vorhanden. Im Zweig Production, werden alle Module definiert sein, die getestet, etabliert und keine Fehler mehr aufweisen. Im Bereich Staging alle Module, die noch umfassend getestet werden müssen, um in die Production zu kommen. Eventuell aber sogar nich den vollen Funktionsumfang aufweisen. Im Development Zweig dementsprechend alle Module, die momentan entwickelt werden.

Das Modul ist der ausführbare Code


Die oben realisierte Projektstruktur wird versioniert und ist dann schnell auf den Provisioner Server geclont. Mit Tags können zudem freingranularere Strukture geschaffen werden.

Nehmen wir als Versionierungslösung GIT. Der Code wird dann geclont (git clone ...) und ist dann für Terraform sichtbar.

Die ganze Infrastruktur auf einmal erzeugen


Angenommen es gebe ein Modul "create-infrastructure", was alle Sub-Module einbettet, die für die Erzeugung der gesamten Infrastruktur zuständig sind. Zuerst einmal muss in dieses Verzeichnis gewechselt werden. Mit
- terraform init
- terraform plan
und schliesslich mit
- terraform apply

wird die Infrastruktur erzeugt.

Vorteile:
- Es muss nur einmal terraform initialisiert werden
- Es wird mit nur einem Terraform State-File gearbeitet

Leider zeigt sich in der Praxis, dass dieser Ansatz nicht optimal ist. Den wenn das Projekt wächst, so werden auch die Abhängigkeiten unter den Modulen wachsen. Terraform arbeitet parallel, was dazu führen kann, dass ein Run von terraform apply nicht langt. Apply muss mehrmals hintereinander ausgeführt werden. Auch der Entwickler muss dann immer den ganzen Code asführen lassen, um ein Submodul zu testen.

Nachteile:
- Terraform bleibt hängen und benötigt mehrere Durchgänge
- Grosse Abhängigkeiten von Sub-Modulen, die schon in fertiger (ausgetester Form) vorhanden sein müssen
- Nachteile für den Entwickler

Die ganze Infrastruktur in mehreren Schritten erzeugen


Ein anderer Ansatz ist, dass die Infrastruktur mit mehren separat ausführbaren Modulen erzeugt wird.
Also anstatt terraform init, plan und apply auf einem Modul auszuführen, muss das für jedes Modul getan werden. Das gibt zusätzlichen Aufwand, der aber gar nicht so gross ist. Meistens werden nämlich mit jedem separat ausführbaren Modul zusätzliche Abhängigkeiten eliminiert.

Beispiel:

Modul "create-resource-groups" 
... soll die Resourcengruppen erzeugen
Modul "create-subnets"
... soll die Subnetze erzeugen

Das Modul für die Subnetze benötigt aber für jedes Subnetz eine Resourcengruppennamen. Der wurde vorher schon vom ausgetesteten Module "create-resource-groups" erzeugt. Beim Testen muss also nur einmal das Modul "create-resource-groups" ausgeführt werden und alle abhängigen Module können dann separat getestet werden, ohne noch einmal wie bei der "Integrallösung" die Ressourcengruppen jedesmal zu erzeugen.

Vorteile:
- Schnellere Testbarkeit
- Abhängigkeiten werden explizit durch die richtige Reihenfolge der Ausführung der Module eliminiert

Nachteile:
- Grösserer Aufwand beim Deployment
- Mehrere State-Files

Hybride Struktur


Es ist auch vorstellbar, dass eine hybride Struktur zum Einsatz kommt, die im Wesentlichen auf die Variante zwei bei der Entwicklungsphase setzt und dann auf Variante eins beim Deployment.


Immer wieder auftretende Probleme


Bei der praktischen Anwendung von Terraform, zeigt es sich, dass immer wieder gewisse Aufgaben vorkommen, die nicht oder nur schwer mit Boardmitteln von Terraform realisiert werden können. Einige Lösungsansatze beschreibe ich unten:

Die Sache mit den Tags


Eine Ressource kann meistens auch Tags enthalten. Tags sind anzugeben als Map. Meistens werden sie aber ihre Ressource ohnehin via Map parametrisieren. Das Problem ist nun, dass Terraform folgendes Konstrukut beispielsweise nicht unterstützt:


locals {
   mymap = "${ map ("name", "vm_1", "tags", map ("tagkey", "tagvalue1", "tagkey2", "tagvalue2"))}"
}

Es kann aber umgeschrieben werden zu:


locals {
   mymap = "${ map ("name", "vm_1", "tags", "tagkey,tagvalue1,tagkey2,tagvalue2")}
}

Dabei werden alle Tags innerhalb eines Strings gekapselt. Damit der Aufwand nicht zu gross wird, um dann die Tags zuzuweisen muss der Tag-String ohne Lücken als beispielsweise nicht "tagkey, tagvalue" sondern als "tagkey,tagvalue" eingegeben werden. Ansonsten ist da ein Space vorhanden, der dann auch noch elimiert werden muss.

Die Tags werden dann wie folgt in der Ressource verwendet:



  tags = "${zipmap( null_resource.tags.*.triggers.keys, null_resource.tags.*.triggers.values)}"

  data "template_file" "tagslist" {  
      
      template  = "$${tags_as_list}"
      vars {
        tags_as_list = "${var.mymap["tags"]}" 
      }
  }

  locals {
      element_counter = "${length(split(",", data.template_file.tagslist.rendered))}"
  }


  resource "null_resource" "tags" {
     count = "${local.element_counter/2}"
  
     triggers = {
        keys   = "${element( split(",", data.template_file.tagslist.rendered), count.index*2)}"
        values = "${element( split(",", data.template_file.tagslist.rendered), count.index*2+1)}"
     }
  }

 

Die Sache mit Count


Soll eine Ressource mehrmals erzeugt werden, so kann mit Listen von Maps gearbeitet werden, wobei eine Map gerade die Ressource vollständig definiert.


   locals {
      vms_map = "${
            list (
               map (# hier die Map für die Ressource der 1.ten virtuellen Maschine),
               map (# hier die Map für die Ressource der 2.ten virtuellen Maschine)
            )
      }"
   }

   resource "azurerm_virtual_maschine", "vm" {
       count = "${length(local.vms_map)}"  
  
       # Zugriff auf ein Element der Map
       lcoation = "$(lookup(local.vms_map[count.index], "location")}"
   }

Das Problem hier ist, dass meistens die Meldung kommt, dass count nicht "computed" werden kann. Der Fehler tritt in zwei Ausprägungen auf:

1) Der Code ist fehlerhaft und der Terraform Parser gibt diese Fehlermeldung aus
2) Die Fehlermeldung ist so zu nehmen wie sie ist.

Abhilfe schafft hier die Einführung eines explititen Wertes, der Count übergeben wird:


  module "create-a-vm" {
     source ="../../vm"
     maplength = 5
     map = "${...}" 
  }

  und in der Datei main.tf im Verzeichnis vm

  resource "azurerm_virtual_maschine", "vm" {
       count = "${var.maplength}
       ...
  }

 

Die Sache mit count in Modulen


Ein wesentlches Defizit von Terraform ist das Fehlen der Variablen count bei der Einbindung eines Moduls. Es kann also nicht "count-mal" eingebunden werden. Das wäre aber praktisch, denn dann könnte gerade ein Bulk von Ressourcen innerhalb einer einzigen Modul-Definition erzeugt werden.

Wenn allerdings die Infrastruktur in mehrern Schritten erzeugt werden soll wie unter "Die ganze Infrastruktur in mehreren Schritten erzeugen" beschrieben gibt es eine Lösung. Der Code für eine einzelne Ressource wird in je eine separate Terraform Datei in ein Verzeichnis gerendert und in diesem Verzeichnis wird dann teraform init, plan und apply ausgeführt:

Allerdings ist das Rendern hochkomplex und wird nicht empfohlen!


locals {
  lbrace = "{"
  rbrace = "}"
}

data "template_file" "vmdef" {  
  count     = "${var.instance_count}"
  
  template  = "$${vm_t_line1}$${vm_index}\"\n$${vm_t_line2}\n$${vm_t_line3}\n$${vm_t_line4}\n$${vm_t_line5}\n$${vm_t_line6}\n$${vm_t_line7}{\n$${test}}\"\n$${vm_t_line8}"
  vars {
    vm_index = "${count.index}"
    vm_t_line1 = "module \"create-vm-" 
    vm_t_line2 = "{    source = \"../../../prod/templates/compute/virtual-machine/vm\" "
    vm_t_line3 = "     vm_definitionsmap = \"$${local.vm_definitionsmap_${count.index}}\""
    vm_t_line4 = "     instance_count = 1 "
    vm_t_line5 = "}"
    vm_t_line6 = "locals {"
    vm_t_line7 = "     vm_definitionsmap_${count.index} =\"$$"
    vm_t_line8 = "}"
    test =  "${replace(replace(replace(jsonencode(var.vm_definitionsmap[count.index]), local.rbrace, ")"), local.lbrace, "map("),":",",")}"
  }
}

resource "local_file" "foo" {
    count = "${var.instance_count}"

    content     = "${element(data.template_file.vmdef.*.rendered, count.index)}"
    filename = "./loop/foo-${count.index}.tf"
}


resource "null_resource" "terraform-init" {
  provisioner "local-exec" {
    working_dir = "./loop"
    command = "terraform init"
  }
}

resource "null_resource" "terraform-apply" {
  provisioner "local-exec" {
    working_dir = "./loop"
    command = "terraform apply -auto-approve"
  }
  depends_on = ["null_resource.terraform-init"]
}

und der Aufruf erfolgt dann wie folgt

module "create-vms" {

    source = "../../prod/templates/compute/virtual-machine"
     providers = {
        "azurerm" = "..."
    }
    vm_definitionsmap = "${local.local_vmdefinitionsmap}"
    instance_count = "10"
}


Das erzeugt im Unterverzeichnis loop für jede zu erzeugende Ressource eine Datei foo-n.tf, wobei n gerade die n-te erzeugende Ressource bezeichnet.

Keine Kommentare:

Kommentar veröffentlichen