Programmieren statt Konfigurieren: Infrastruktur als TypeScript-Code

Seite 3: Der Code im Detail

Inhaltsverzeichnis

Es folgt eine detaillierte Erklärung zum Code des Stacks. Zuerst sind einige Abhängigkeiten einzufügen, darunter mehrere AWS Construct Libraries (Listing 3).

import { Duration, Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'
import * as loadbalancing from 'aws-cdk-lib/aws-elasticloadbalancingv2'
const fs = require('fs');

Listing 3: Die importierten AWS Construct Libraries für EC2, Auto Scaling und Load Balancing sind Teil der CDK-Hauptbibliothek.

Die AWS Construct Libraries enthalten die bereits erwähnten Constructs-Klassen, die die Infrastruktur repräsentieren. Eine Besonderheit ist, dass Constructs unterschiedlich komplex beziehungsweise intelligent sein können. Diese Eigenschaft heißt Layer. Layer-1-Constructs entsprechen genau der Ressource aus dem IaC-Werkzeug CloudFormation und umfassen keine zusätzlichen Funktionen. Layer-2-Constructs sind deutlich interessanter: Sie repräsentieren ebenfalls AWS-Ressourcen, bieten aber eine höhere Abstraktion mit Standardwerten, typischen Konfigurationsmustern und Hilfsfunktionen. Im Beispiel aus Listing 1 sind hauptsächlich Constructs vom Typ Layer 2 vorhanden. Wie einfach es Layer-2-Constructs machen, Infrastruktur zu definieren und sinnvoll zu konfigurieren, zeigt die folgende Zeile in der Constructor-Funktion des Stacks. Dank eingebauter, sinnvoller Voreinstellungen lässt sich ein privates Netzwerk (VPC) mit nur dieser einen Zeile Code erstellen:

const vpc = new ec2.Vpc(this, 'MyVPC')

Das vpc Construct übernimmt das Erstellen und Konfigurieren der Virtual Private Cloud (VPC), also des privaten Netzwerks. Beim Eintippen in der eigenen IDE macht die Inline-Dokumentation deutlich, was das Construct leistet. So setzt es ohne weiteres Zutun eine VPC nach Best Practices mit privaten und öffentlichen Subnetzen über mehrere Availability Zones auf. Nur zwei Parameter sind zwingend zu übergeben: eine Referenz zum darüber liegenden Construct (oft ist das der Stack) und ein eindeutiger Name. Generell benötigen alle Constructs diese beiden Parameter.

Als Nächstes wird die Auto Scaling Group definiert. Wie für die VPC wird dazu eine Construct-Klasse instanziiert, nur sind diesmal einige zusätzliche Parameter zu übergeben (Listing 4).

    const asg = new autoscaling.AutoScalingGroup(this, 'MyASG', {
      vpc: vpc,
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MICRO),
      machineImage: ec2.MachineImage.latestAmazonLinux({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022 }),
      vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_NAT }),
      minCapacity: 2
    })

Listing 4: Hilfsfunktionen und vordefinierte Enums vereinfachen die Definition der Auto Scaling Group.

Zum einen ist das vpc-Objekt erforderlich. Zum anderen müssen die Angabe des Instanztyps und die des zu startenden Images folgen. Das Beispiel verwendet den Burstable-Typ, der besonders kostengünstig ist. Als Image kommt das neueste Amazon Linux 2022 zum Einsatz. Es ist nicht nötig, die Konfigurationswerte händisch auszuschreiben. Stattdessen stehen Hilfsfunktionen wie InstanceType.of() und MachineImage.latestAmazonLinux() zur Verfügung, die Enums wie InstanceClass.BURSTABLE3 aufnehmen. Das reduziert Fehler und die für das Suchen nach korrekten Werten benötigte Dauer.

Zudem wird noch angegeben, in welchen Subnetzen die EC2-Instanzen laufen sollen. Auch hierfür gibt es eine praktische Hilfsfunktion selectSubnets(), um die privaten Subnetze explizit auszuwählen. Zuletzt wird mittels minCapacity noch festgelegt, dass mindestens zwei Instanzen zu jeder Zeit aktiv sein sollen.

Als Nächstes folgt das Hinzufügen des Installationsskripts für den Webserver als User Data für die Autoscaling Group. Die Instanzen führen beim Hochfahren das übergebene Skript aus, um den Webserver zu installieren. Dafür bietet die AutoScalingGroup-Klasse eine passende Hilfsfunktion.

asg.addUserData(fs.readFileSync('scripts/install.sh', 'utf8'))

Die Funktion addUserData() erwartet einen String, aber das Skript ist als separate Datei gespeichert. Deswegen kommt die eingebaute fs-Bibliothek von Node.js zum Einsatz, um die Datei zuvor zu lesen. Für Node.js-Entwicklerinnen und -Entwickler ist das intuitiv. 

Jetzt fehlt nur noch der Application Load Balancer, um die Architektur zu vervollständigen. Dem ApplicationLoadBalancer-Construct wird erneut die VPC übergeben. Zudem wird der Load Balancer so konfiguriert, dass er aus dem Internet erreichbar ist. Danach sind mehrere Hilfsfunktionen daran beteiligt, die Ports des Load Balancer mit den Instanzen der Auto Scaling Group zu verknüpfen. So öffnet addListener() den HTTP-Port am Load Balancer, und addTargets() verknüpft ihn mit den entsprechenden Ports der EC2-Instanzen. Zuletzt konfiguriert allowDefaultPortFromAnyIpv4() die Security Groups so, dass eingehender Datenverkehr aus dem Internet erlaubt ist (Listing 5).

const alb = new loadbalancing.ApplicationLoadBalancer(this, 'MyALB', {
      vpc : vpc,
      internetFacing : true
    })
    
    const listener = alb.addListener('HttpListener', {
      port: 80
    })
    
    listener.addTargets('Targets', {
      port: 80,
      targets: [asg]
    })
    
    listener.connections.allowDefaultPortFromAnyIpv4('Allow access to port 80 from the internet.')

Listing 5: Die "sprechenden" Hilfsfunktionen des CDK lassen die eigentliche Intention schneller erkennen.

Die letzte Zeile des Stacks ist nicht zwingend notwendig, vereinfacht es aber, den Hostnamen des Load Balancer zu erfahren. Die Klasse CfnOutput ermöglicht es, nach dem Deployment des CloudFormation-Templates weitere Informationen über die Infrastruktur auszugeben. In diesem Fall soll der Wert für die selbsterklärende Eigenschaft loadBalancerDnsName als Ausgabe erscheinen. Mit der nachfolgenden Zeile gibt das CDK CLI nach Fertigstellung der Infrastruktur in der Cloud den Hostnamen des Load Balancer aus:

    new CfnOutput(this, 'Hostname', {value:
alb.loadBalancerDnsName})

Im Beispiel entsteht mit dem Cloud Development Kit eine Infrastruktur in 30 Zeilen, die in einem traditionellen IaC-Werkzeug mindestens 300 Zeilen beansprucht. Sind damit die Tage von CloudFormation und Terraform gezählt? Die Antwort ist ein klares Nein. Das CDK ist insbesondere für Kenner einer der unterstützten Programmiersprachen wie TypeScript interessant. Zudem hat das Beispiel gezeigt, dass Constructs mächtige Abstraktionen sind, die das Erstellen einer Architektur schneller und in höherer Qualität ermöglichen. Trotzdem bleiben die bestehenden, deklarativen Werkzeuge wie CloudFormation und Terraform relevant. Für sie gibt es unzählige Informationsquellen und bestehendes Know-how. Teams sollten also nach ihren Fähigkeiten und Vorlieben wählen, statt einem Mandat für ein bestimmtes Werkzeug zu folgen.