Nahaufnahme einer Person, die mit einem Tablet auf einer Decke sitzt.

TechWiese Blog

Deployment von Azure Kubernetes Service (AKS) mit TypeScript unter Verwendung des Cloud Development Kit (CDK) für Terraform

20. August 2020

Portrait Bild von Mark Warneke

Mark Warneke

Dieser Blogbeitrag ist ein Repost und stammt im Original aus dem Know-how-Bereich von TechWiese, dessen Artikel in diesem Blog aufgegangen sind.

HashiCorp hat vor kurzem das CDK for Terraform: Enabling Python and TypeScript Support angekündigt - CDK steht für das Cloud Development Kit. Das CDK ermöglicht Cloud-Engineers Cloud-Infrastrukturen in Form von einer Höheren Programmiersprachen wie TypeScript oder Python als Infrastructure as Code (IaC) zu definieren.

Das CDK besteht derzeit aus einer CLI und einer Bibliothek zur Definition von Terraform-Ressourcen unter Verwendung von TypeScript oder Python. Das CDK wird zur Generierung von Terraform-Konfigurationsdateien, die zur Bereitstellung von Ressourcen verwendet werden können, genutzt.

In diesem Blog-Beitrag werde ich in das CDK eintauchen und die bestehenden Azure-Provider nutzen, um einen Azure Kubernetes Service (AKS) unter Verwendung von TypeScript zu erstellen. Der gesamte Code kann zum Nachvollziehen auf meinem Github Profile gefunden werden.

Cloud Development Kit (CDK) for Terraform auf Azure

TLDR: Das CDK für Terraform ergänzt das bestehende Terraform Ökosystem. Das CDK ergänzt dabei die folgenden Fähigkeiten:

  • compiler (Nutzung einer stark typisierten Programmiersprache)
  • debugger (Verbesserte Entwickler-Schleife)
  • extension (Verwendung von Lintern, Bibliotheken, Paketen und Software-Design-Pattern)

Das CDK ist derzeit in Node implementiert und kann mit npm install -g cdktf-cli installiert werden, siehe getting started.

CDK-Übersicht, wie sich das CDK in das Terraform Ökosystem eingliedert. Zu sehen ist die bestehenden Terraform Funktionen, wie z.B. JSON & HCL - nun erweitert um das CDK mit TypeScript und Python

Schauen wir uns die cdktf-cli-Befehle genauer an:

cdktf [command]

Commands:
  cdktf deploy [OPTIONS]   Deploy the given stack
  cdktf destroy [OPTIONS]  Destroy the given stack
  cdktf diff [OPTIONS]     Perform a diff (terraform plan) for the given stack
  cdktf get [OPTIONS]      Generate CDK Constructs for Terraform providers and modules.
  cdktf init [OPTIONS]     Create a new cdktf project from a template.
  cdktf login              Retrieves an API token to connect to Terraform Cloud.
  cdktf synth [OPTIONS]    Synthesizes Terraform code for the given app in a directory. [aliases: synthesize]

Options:
  --version          Show version number [boolean]
  --disable-logging  Dont write log files. Supported using the env CDKTF_DISABLE_LOGGING.                                                                             [boolean] [default: true]
  --log-level        Which log level should be written. Only supported via setting the env CDKTF_LOG_LEVEL [string]
  -h, --help         Show help [boolean]

Options can be specified via environment variables with the "CDKTF_" prefix (e.g. "CDKTF_OUTPUT")

Um zu beginnen, erstellen wir einen neuen Ordner und führen cdktf init --template="typescript" aus. Dadurch wird eine TypeScript-Dateistruktur generiert, sowie die erforderlichen Abhängigkeiten heruntergeladen.

Die Ausgabe zeigt mehrere erzeugte Dateien, einschließlich der bekannten main (in diesem Fall main.ts statt main.tf) und der CDK-Konfigurationsdatei cdktf.json

Die Ausgabe zeigt mehrere erzeugte Dateien, einschließlich der bekannten main (in diesem Fall main.ts statt main.tf) und der CDK-Konfigurationsdatei cdktf.json. Zur Nutzung von Azure mit CDK, fügen wir den Azure-Provider zur cdktf-Konfigurationsdatei hinzu.

Zur Nutzung von Azure mit CDK, fügen wir den Azure-Provider zur cdktf-Konfigurationsdatei hinzu

Die terraformProvider können in der Terraform-Registry z.B. azurerm gefunden werden. Nachdem wir "azurerm@~> 2.0.0" zum cdkf.json hinzu gefügt haben, können wir cdktf get ausführen um den Azure-Provider herunterzuladen.

Alle verfügbaren Provider und Ressourcen werden in ./.gen/providers/ abgelegt.

Stellen Sie sicher, dass Sie sich bei Azure mit der Azure-cli az login einloggen. Das interaktive ausführen der cdktf, ähnlich wie terraform, verwendet standardmäßig den aktuellen Azure-Kontext.

Kubernetes auf Azure mitteils TypeScript bereitstellen

Nachdem die Provider installiert wurden, kann der Provider unter .gen/providers/azurerm eingesehen werden. Die Definition aller verfügbaren Ressourcen finden wir hier! Besipielsweise die Kubernetes Resource kann mittels des folgenden Befehls gefunden werden:

ls ./.gen/provider/azurerm | grep "Kubernetes"

Schauen wir uns die TerraformResource-Implementierung nun etwas genauer an. Um die Implementierung zu sehen, Nutzen Sie einen Code-Editor um kubernetes-cluster.tszu öffnen.

less ./.gen/provider/azurerm/kubernetes-cluster.ts

Sie suchen nach der exportierten Klasse KubernetesCluster: export class KubernetesCluster extends TerraformResource.

KubernetesCluster TerraformResource

Um die Kubernetes TerraformResource zu benutzen, deklarieren wir die Klasse innerhalb von main.ts. Fügen Sie zunächst die Abhängigkeit zu dem Import hinzu:

Import { AzurermProvider, KubernetesCluster} aus './.gen/providers/azurerm'.

Die main.ts sollte in etwa so aussehen:

import {Construct} from "constructs";
import {App,TerraformStack,TerraformOutput} from "cdktf";
import {AzurermProvider,KubernetesCluster} from "./.gen/providers/azurerm";
class K8SStack extends TerraformStack {
  constructor(scope: Construct,name: string){
    super(scope,name);

    // Register the AzureRmProvider, make sure to import its
    const provider = new AzurermProvider(this,"AzureRm",{
      features: [{}],
    });

    // ...

    new KubernetesCluster(this,"k8scluster",{
      // ... KubernetesClusterConfig}
    );
  }
}

const app = new App();
const k8tstack = new K8SStack(app,"typescript-azurerm-k8s");
app.synth();

TerraformResource akzeptiert einen scope, eine id und einen config Parameter. Die Konfiguration hängt von der bereitzustellenden Ressource ab - dabei basiert der Klassenname auf der Ressource gefolgt von Config. Für ein KubernetesCluster suchen wir daher nach KubernetesClusterConfig.

In diesem Beispiel wird der scope mit this auf den aktuellen TerraformStack gesetzt. Die id ist ähnlich wie der Ressourcenname in Terraform zu benutzen und sollte eindeutig sein. Die config ist eine Implementierung von TerraformMetaArguments, im Folgenden betrachten wir, wie die KubernetesClusterConfig benutzt wird.

Definieren der KubernetesClusterConfig

Die KubernetesClusterConfig ist eine Interface, dass die TerraformMetaArguments für einen Kubernetes-Cluster beschreibt.

export interface KubernetesClusterConfig extends TerraformMetaArguments

Die Schnittstelle beschreibt die Eigenschaften der Konfiguration. Wenn Sie einen Code-Editor wie VSCode verwenden und mit der Umschalttaste auf KubernetesClusterKubeConfig klicken, wird die Implementierung des Interfaces angezeigt.

Wir können sehen, welche Eigenschaften für die Konfiguration obligatorisch und welche optional sind. Optionale Eigenschaften werden mit einem Fragezeichen ? nachfixiert, z.B. readonly apiServerAuthorizedIpRanges?: string[];.

Wir können auch intellisense des verwendeten Editors verwenden, um fehlende Variablen anzuzeigen und vorzuschlagen. Die obligatorische Konfiguration für eine KubernetesClusterConfig sieht wie folgt aus:

const k8sconfig: KubernetesClusterConfig={
  dnsPrefix: AKS_DNS_PREFIX,
  location: LOCATION,
  name: AKS_NAME,
  resourceGroupName: rg.name,
  servicePrincipal: [ident],
  defaultNodePool: [pool],
  dependsOn: [rg],
};

Achtung, wir beziehen uns hier auf Variablen und nicht auf feste, hartkodierte Werte. Die resourceGroupName zum Beispiel verweist auf ein Attribut einer zuvor definierten ResourceGroup rg, usw. Zur Referenz ist die Implementierung in generate terraform abgebildet.

Wir können die offiziellen Dokumente des Terraform-Providers für einen Kubernetes-Cluster terraform.io/azurerm_kubernetes_cluster überprüfen und sehen, dass die Konfigurationswerte mit den obligatorischen Parametern der Argument-Referenz übereinstimmen.

Angenommen, wir haben eine obligatorische Eigenschaft übersehen! Der TypeScript-Compiler wird einen Fehler ausgeben und frühzeitig anzeigen, dass ein Attribut übersehen wurde. Dies ist ein großer Vorteil der stark typisierten Programmiersprache TypeScript gegenüber einer losen Konfigurationsdatei.

# tsc

main.ts:36:18 - error TS2304: Cannot find name 'AKS_DNS_PREFIX'.

36       dnsPrefix: AKS_DNS_PREFIX,

Notiz: Auch natives Terraform unterstützt die statische Validierung von HCL-Dateien via terraform validate. Weitere Werkzeuge wie z.b.tflint sind darüber hinaus verfügbar.

Um ein Konfigurationselement zu erstellen, verwenden Sie let NAME: Typ = {}. Das TypeScript-Objekt kann dann auf der Grundlage von Bedingungen mit zusätzlichen Eigenschaften erweitert werden, genau wie jedes andere TypeScript-Objekt.

Der Editor kann zur Autovervollständigung verwendet werden, z.B. unter Verwendung von Shift-Space, um zusätzliche Eigenschaften des gegebenen Objekts anzuzeigen.

Code-Vorschlag in VSCode welches die Autovervollständigung von Objekt Eigenschaften anzeigt

Notiz: Ein Terraform Azure Kubernetes Cluster kann typischerweise unter Verwendung eines servicePrincipal oder einer identity bereitgestellt werden. Sie schließen sich gegenseitig aus, und mindestens ein Typ muss definiert werden. Bei Verwendung der aktuellen KubernetesClusterConfig ist nur die Eigenschaft servicePrincipal obligatorisch und daher kann Identität alleinstehend nicht verwendet werden. Ich komme zu dem Schluss, dass es derzeit keine 1:1-Abbildung der verfügbaren Terraform-Module auf das CDK gibt - dies ist ein Nachteil und wird, wie ich vermute, in einer späteren Version des CDK behoben werden.

Nutzung von Umgebungsvariablen

Umgebungsvariablen können verwendet werden, um Variablen in eine CDK-Implementierung zu injizieren. In Node.js können Sie process.env verwenden, z.B. process.env.AZ_SP_CLIENT_ID und process.env.AZ_SP_CLIENT_SECRET.


const ident: KubernetesClusterServicePrincipal = {
  clientId: process.env.AZ_SP_CLIENT_ID as string,
  clientSecret: process.env.AZ_SP_CLIENT_SECRET as string,
};

Dies ermöglicht es uns, die Konfiguration zwischen den Implementierungen zu ändern, ohne den Code basierend auf der 12-Faktor-App zu ändern. Die Umgebungsvariablen gesetzt werden durch:

export AZ_SP_CLIENT_ID=''
export AZ_SP_CLIENT_SECRET=''

Terraform erzeugen

Das CDK wird verwendet, um eine Terraform-Datei zu erzeugen. Der Prozess der Erzeugung einer IaC-Datei wird als Synthese bezeichnet. Die bereitgestellte Cli kann verwendet werden, um cdktf synth auszuführen. Dadurch wird ein Verzeichnis cdktf.out erzeugt. In diesem Verzeichnis können wir uns die erstellte Terraform-Datei ./cdktktf.out/cdk.tf.json ansehen.

Die vollständige CDK AKS-Implementierung sieht wie folgt aus:

import {Construct} from 'constructs';
import {App,TerraformStack,TerraformOutput} from 'cdktf';
import {AzurermProvider, KubernetesCluster, KubernetesClusterConfig, KubernetesClusterDefaultNodePool, KubernetesClusterServicePrincipal, ResourceGroup, ResourceGroupConfig} from './.gen/providers/azurerm'

classK8SStack extends TerraformStack {
  constructor(scope: Construct, name: string){
    super(scope, name);

    const provider = new AzurermProvider(this, 'AzureRm', {
      features: [{}]
    })

    const LOCATION = 'westeurope'
    const RG_NAME = 'mwtestmarkmitk'
    const AKS_NAME = 'mwtestmarkmitk'
    const AKS_DNS_PREFIX = 'mwtestmarkmitk'
    
    const rgConfig: ResourceGroupConfig={
      location: LOCATION,
      name: RG_NAME
    }
    const rg = new ResourceGroup(this, 'k8scluster-rg', rgConfig)

    const pool: KubernetesClusterDefaultNodePool = {
      name: 'default',
      vmSize: 'Standard_D2_v2',
      nodeCount: 1
    }
    
    const ident: KubernetesClusterServicePrincipal = {
      clientId: process.env.AZ_SP_CLIENT_ID as string,
      clientSecret: process.env.AZ_SP_CLIENT_SECRET as string
    }
    
    const k8sconfig: KubernetesClusterConfig = {
      dnsPrefix: AKS_DNS_PREFIX,
      location: LOCATION,
      name: AKS_NAME,
      resourceGroupName: rg.name,
      servicePrincipal: [ident],
      defaultNodePool: [pool],
      dependsOn: [rg],
    };
    
    const k8s = new KubernetesCluster(this, 'k8scluster', k8sconfig)
    
    
    const output = new TerraformOutput(this, 'k8s_name', {
      value: k8s.name
    });
    
    console.info(rg.name,k8s.name,provider.subscriptionId,output.friendlyUniqueId)
  
  }
}

const app = new App();
const k8tstack = new K8SStack(app, 'typescript-azurerm-k8s');
console.info(k8tstack.toString())
app.synth();

cdktf synth kann auch mit dem Flag -o ausgeführt werden um die Ausgabedatei zu spezifizieren. Mit dem Flag -json wird die erzeugte Terraform-Datei einfach auf der Konsole ausgegeben. cdktf erzeugt json, da es nicht notwendig ist menschenlesbar zu sein. Terraform arbeitet sowohl mit hcl (.tf) als auch mit json (.json) Dateien.

Wenn wir die Datei cdk.tf.json untersuchen, können wir die bekannte Terraform-Struktur erkennen. Alle Ressourcen sind vorhanden, einschließlich der Werte & der zuvor gesetzten Werte aus den Umgebungsvariablen, z.B. die service principal id und das secret.

Geheimnisse sind hier sichtbar. Stellen Sie sicher, dass geeignete Maßnahmen ergriffen werden, um deren Durchsickern zu verhindern. Verwenden Sie Azure Key Vault azure_key_vault_secret, um Geheimnisse sicher zu speichern und abzurufen. Das Ein-Checken der Ausgabe des Synth-Schrittes sollte verhindert werden, z.B. durch die Nutzung einer .gitignore-Datei.

Wir können cdktf diff ähnlich wie terraform diff benutzen, um die vorzunehmenden Änderungen anzuzeigen bevor diese schlussendlich angewendet werden. Wir können auch den bekannten Terraform-Status im Root-Ordner terraform.tfstate untersuchen. Der Zustand kann z.B. in einem Remote-Backend konfiguriert werden, wie in den Dokumenten für Terraform Remote Backend beschrieben.

Bringen Sie es zum Laufen

# Melden Sie sich bei Azure an, um den lokalen Terraform-Kontext einzustellen
az login

# Exportieren Sie die "nicht so" geheimen Umgebungsvariablen
export  AZ_SP_CLIENT_ID=''
export  AZ_SP_CLIENT_SECRET=''

# Generieren der Terraform-Bereitstellungsdatei
cdktf synth

# Führen Sie den Diff aus, um geplante Änderungen zu sehen
cdktf diff

# Terraform-Anwendung unter Verwendung von cdkt der generierten Terraformdatei ausführen
cdktf deploy

# Anschließend Zerstören der Bereitstellung
cdktf destroy

Da das CDK zur Generierung von Terraform-Bereitstellungsdateien verwendet wird, können wir die aus dem syntese Schritt generierten Terraform-Dateien mit der bekannten Terraform-CLI verwenden.

Wechseln wir in cdktf.out und führen terraform validate, terraform plan und terraform apply aus. Das ist die Magie hinter der Synthetisierung der Terraform-Konfiguration mit dem CDK für Terraform. Dieses Tooling generiert gültigen Terraform-Code, der leicht zu bestehenden IaC-Projekten hinzugefügt werden kann. Zuvor erstellte Pipelines können weiterhin zur Bereitstellung der Infrastruktur verwendet werden.

Wir können das CDK nutzen, um die Erstellung der Bereitstellungsdatei in einer höheren Programmiersprache zu abstrahieren, ohne zu viel in das Refactoring von vorhandener Tools und Pipelines zu investieren und ohne auf die Vorteile der IaC-Konfigurationsdateien verzichten zu müssen.

Ausguck

Da eine übergeordnete Programmiersprache verwendet wird, können wir eine Reihe von Tools nutzen, die im IaC-Entwicklungslebenszyklus bisher gefehlt haben.

Compiler

Die Verwendung der stark typisierten Sprache wie TypeScript als Vermittler kann verwendet werden, um Konfigurationsfehler frühzeitig abzufangen. Der TypeScript-Compiler kann in der Entwickler-Schleife verwendet werden um mittels der Ausführung von tsc im Root-Ordner alle TypeScript-Fehler sofort anzuzeigen.

Fehlende Pflichtvariablen oder falsche Zuweisungen gehören dadurch der Vergangenheit an.

Linter

Werkzeuge wie tslint können verwendet werden, um statische Code-Analysen durchzuführen und sicherzustellen, dass Inkonsistenzen und Fehler frühzeitig erkannt werden. Wenn Sie tslint -c tslint.json main.ts ausführen, wird jede Verletzung der konfigurierten Regeln angezeigt. Linter können auch verwendet werden, um eine Codebasis zu vereinheitlichen, dies ist besonders interessant bei mehreren Beitragenden zum gleichen Code.

Bibliotheken und Werkzeuge

Werkzeuge und Bibliotheken wie hcl2json oder json2hcl können verwendet werden, um das CDK zu erweitern und zu ergänzen. Abstraktionen und Design-Pattern können dazu beitragen eine große IaC-Code Basis zu vereinfachen und wiederverwendbar zu gestalten. Z.B. durch die Erstellung von NPM Packeten.

Debugger

Da außerdem eine richtige Programmiersprache, im Vergleich zu einer Konfigurationssprache, verwendet wird, können wir Debugger in unseren Entwicklungs-Workflow nutzen.

Screenshot einer Debugger-Session innerhalb von VSCode

(In VSCode öffnen Sie main.ts > Starten Sie den Debugger mit F5 > Wählen Sie die Umgebung Node.js)

Die Fehlerbehebung bei der Zuweisung von Variablen, das Verständnis komplexer Schleifen, Bedingungen sowie die Auflösung von Abhängigkeitenn sind durch den Einsatz eines Debuggers wesentlich einfacher nachzuvollziehen und schlussendlich zu lösen. Das Erkennen und Beheben von Fehlern sollte weniger mühsam sein, da endlich vorhandene und lang bestehende Software-Entwicklungswerkzeuge für die IaC-Entwicklung verwendet werden können.

Tests

Komplexe Terraform-Dateien können in kleine, wiederverwendbare Stücke zerbrochen werden. Ein komplexe Terraform Beretistellung kann dynamisch konstruiert werden. Mehrere Module können mit Hilfe von Schleifen und Bedingungen zusammengestellt werden. Die Menge an Konfigurationscode kann erheblich reduziert werden, z.B. durch den Einsatz von Vererbung und Polymorphie. Unit-Tests können angewendet werden, um sicherzustellen, dass die generierten Vorlagen korrekt und konsistent sind.

Ausblick

Durch die Kombination von parametrisierten Terraform-Modulen und die Verwendung des CDK können Implementierungen in einfach zu verwendende Dienste abstrahiert werden.Z.B. Befehlszeilenwerkzeuge, gemeinsam genutzte APIs und Webdienseiten können mit Hilfe des CDK nativ umgesetzt werden. Diese Dienste erstellen reproduzierbare IaC auf Grundlage von Terraform Konfigurationsdaten. Variablen und Parameter können nun sogar in ausgelagerten Datenbanken gespeichert und abgerufen werden.

Die benutzerdefinierte Validierung und die Durchsetzung der Namenskonvention durch benutzerdefinierten Funktionen können zur weiteren Skalierung und Reifung von IaC-Projekten verwendet werden.

Die Flexibilität einer vollständigen Programmiersprache - dabei die idempotenten und deklarativen Charakter von IaC trifft auf einander.

Den vollständigen Code finden Sie unter github.com/MarkWarneke/cdk-typescript-azurerm-k8s. Den englischen Blog-Beitrag finden Sie unter markwarneke.me.