In diesem Blog-Artikel möchte ich einen Ansatz vorstellen, wie man eine Webanwendung mit AWS Cloudformation in die AWS Cloud deployt, die trotz möglicher Ausfälle von Availability Zones und Servern hochverfügbar bleibt. Ausgangspunkt für diesen Blog Post ist eine bereits vorhandene Netzwerkarchitektur (siehe Bild 1), sodass ich im Folgenden den Fokus vor allem auf die Rolle und Funktionsweise des Load Balancers legen werde. Alle Komponenten, auf die ich hier nicht genauer eingehen werde, können in meinem GitHub Repository nachgeschaut werden.

Zuallererst möchte ich einmal kurz das obige Bild beschreiben. Wenn man seine Anwendung in die Cloud deployen möchte, ist es aus Sicherheitsgründen sinnvoll, dafür Server in privaten Subnetzen zu verwenden. Und da eine Availability Zone mal nicht erreichbar sein kann, verwenden wir zwei private Subnetze. Beide jeweils in einer anderen Availability Zone. Damit unsere Server – die wir später in die beiden privaten Subnetze installieren – mit der Außenwelt kommunizieren können, verwenden wir sogenannte NAT-Gateways. Aus den gleichen Gründen wie bei den privaten Subnetzen verwenden wir auch zwei öffentliche Subnetze, denen wir jeweils ein NAT-Gateway zuordnen. Mit Routing Tabellen für die privaten und öffentlichen Subnetze kann definiert werden, dass Server aus den privaten Subnetzen mithilfe des NAT-Gateways das Internet Gateway erreichen können, umgekehrt jedoch nicht.
Das Ziel ist es nun, mit einem Load Balancer – zu Deutsch „Lastverteiler“ – die hereinkommenden Anfragen auf unsere Server in den privaten Subnetzen zu verteilen, damit unser System als Ganzes auch bei höherer Last verfügbar bleibt. Und nicht nur das. Der Load Balancer soll den Health Status unserer Server überwachen und bei Nichterreichbarkeit eines Servers einen neuen Server anhand eines von uns definierten Templates hochfahren, sodass wir die Lauffähigkeit einer bestimmten Anzahl von Servern sicherstellen können. Wie das geht, schauen wir uns jetzt an.

Target Group
Target Groups werden verwendet, um Anfragen an mehrere Ziele weiterzuleiten. Dabei kann man mehrere Zielgruppen für unterschiedliche Anfragen erstellen. Wir erstellen für unsere Zielgruppe zudem Zustandsprüfungseinstellungen, damit der Load Balancer den Status der einzelnen Instanzen überwachen kann, um bei negativen Health Checks neue Instanzen hochfahren zu können.
- HealthCheckIntervalSeconds: Die Zeit zwischen Checks einzelner Targets.
- HealthCheckPath: Der Pfad an dem der Health Check durchgeführt wird. Antwortet die Anwendung auf dem Server mit http-Status 200 ist alles in Ordnung.
- HealthCheckProtocol: Das Protokoll, das der Load Balancer für die Checks verwendet.
- HealthCheckTimeoutSeconds: Anzahl an Sekunden, nachdem ein Health Check fehlschlägt, wenn bis dahin keine Antwort vom Server gesendet wurde.
- HealthyThresholdCount: Anzahl an Health Checks, bis eine Instanz mit negativem Health Check in den Status healthy wechselt.
- UnhealthyThresholdCount: Anzahl an Health Checks, bis eine Instanz als unhealthy deklariert wird.
WebAppTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 8
HealthyThresholdCount: 2
Port: 80
Protocol: HTTP
UnhealthyThresholdCount: 5
VpcId:
Fn::ImportValue:
Fn::Sub: "${EnvironmentName}-VPCID"
Launch Configuration
Um zu definieren, wie unsere Server aufgebaut sein sollen, verwenden wir eine Launch Configuration. Unter der Property UserData können wir ein Benutzerskript schreiben, das nach Server-Startup ausgeführt werden soll. In unserem Fall installieren wir den Apache Webserver und erstellen eine index.html, die angezeigt wird, wenn wir die entsprechende Adresse später im Browser aufrufen. Zudem geben wir die ID des Amazon Machine Image der Instanz an, sowie die Sicherheitsgruppe und ein 10 GB großes Volume.
WebAppLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
UserData:
Fn::Base64: !Sub |
#!/bin/bash
apt-get update -y
apt-get install unzip awscli -y
apt-get install apache2 -y
systemctl start apache2.service
cd /var/www/html
echo "Demo Web Server Up and Running!" > index.html
ImageId: ami-00d5e377dd7fad751
SecurityGroups:
- Ref: WebServerSecGroup
InstanceType: t2.micro
BlockDeviceMappings:
- DeviceName: "/dev/sdk"
Ebs:
VolumeSize: '10'
Auto Scaling Group
Als Nächstes kümmern wir uns um die Auto Scaling Group. Eine solche Auto Scaling Group ist eine Sammlung von EC2-Instanzen zur automatischen Skalierung und Verwaltung. Hier geben wir unsere privaten Subnetze an, in denen neue Instanzen erstellt werden können. Die zuvor erstellte Launch Configuration gibt an, wie unsere Instanzen aussehen sollen, die in dieser Gruppe sind. Mit den MinSize und MaxSize Properties definieren wir, wie groß diese Gruppe mindestens und maximal sein darf. Haben wir beispielsweise vier laufende Instanzen und eine davon verabschiedet sich, wird direkt eine neue hochgefahren, weil wir als MinSize 4 angegeben haben.
WebAppGroup:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
VPCZoneIdentifier:
- Fn::ImportValue:
!Sub "${EnvironmentName}-PRIV-NETS"
LaunchConfigurationName:
Ref: WebAppLaunchConfig
MinSize: '4'
MaxSize: '4'
TargetGroupARNs:
- Ref: WebAppTargetGroup
Load Balancer
Der Load Balancer ist nicht weiter komplex. Wir geben hier unsere zwei öffentlichen Subnetze und die Sicherheitsgruppe für den Load Balancer an. Die Angabe von zwei Subnetzen ist auch hier wieder AWS Best Practice, um Schwierigkeiten zu umgehen, falls eine Availability Zone nicht erreichbar ist.
WebAppLB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets:
- Fn::ImportValue: !Sub "${EnvironmentName}-PUB1-SN"
- Fn::ImportValue: !Sub "${EnvironmentName}-PUB2-SN"
SecurityGroups:
- Ref: LBSecurityGroup
Listener
Mit dem Listener definieren wir einen Prozess, der mit einem angegebenen Protokoll und Port Verbindungsanforderungen prüft. Unser Listener soll an Port 80 auf HTTP Client-Anfragen warten und diese an unsere Target Group weiterleiten.
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn:
Ref: WebAppTargetGroup
LoadBalancerArn:
Ref: WebAppLB
Port: '80'
Protocol: HTTP
Listener Rule
Mit einer Listener Rule kann man bestimmen, wie der Load Balancer Anforderungen an die Target Groups weiterleitet. In unserem Beispiel leiten wir alle Anfragen von Clients, die unsere Root URL aufrufen, an unsere Target Group weiter. Hätten wir mehrere Target Groups, könnten wir so einkommende Requests anhand eines Pfad-Patterns auf unsere verschiedenen Target Groups aufteilen.
ALBListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref 'WebAppTargetGroup'
Conditions:
- Field: path-pattern
Values: [/]
ListenerArn: !Ref 'Listener'
Priority: 1
Infrastruktur mit AWS Cloudformation bereitstellen
Um die vorgestellte Infrastruktur zu erstellen, muss auf dem eigenen Rechner die AWS CLI installiert sein. Danach sind folgende 3 Schritte notwendig:
git clone https://github.com/mlk4D03/high-availability-webapp-with-cloudformation.git
cd high-availability-webapp-with-cloudformation
./create.sh network-stack network.yml network-parameters.json
./create.sh server-stack server.yml server-parameters.com
Zusammenfassung
Die obige Netzwerkarchitektur haben wir nun dahingehend erweitert, dass wir einen Load Balancer hinzugefügt haben, der jeweils zwei Server in den privaten Subnetzen erstellt und bei Nichterreichbarkeit eines Servers einfach einen neuen hochfährt. Durch diese Architektur haben wir sichergestellt, dass auch durch Ausfall eines Servers oder einer ganzen Availability Zone unsere Anwendung dennoch erreichbar bleibt und auch hohen Nutzerzahlen standhalten könnte. Wer sich von der Funktionsweise des Load Balancers einmal selbst überzeugen möchte, der kann nach Bereitstellung der Server durch AWS Cloudformation per Hand einen Server terminieren. Es sollte nicht lange dauern, bis dies bemerkt wird und ein neuer Server hochgefahren ist.
