Dezember 2024

Eine umfassende Anleitung für Amazon ECS EC2 mit Terraform

Falko Burghausen

Falko Burghausen

In einem der vorherigen Artikel dieser Serie über Cloud Computing in AWS haben wir ECS auf Fargate untersucht. Dieser Artikel behandelt ein sehr ähnliches Setup, das jedoch mehr Flexibilität bietet, gleichzeitig aber auch mit einem höheren operativen Aufwand einhergeht.

In unserer Serie legen wir den Fokus auf Beispiele, die für eine Produktionsumgebung geeignet sind und daher Aspekte wie Netzwerksicherheit, Zugriffsberechtigungen, Skalierbarkeit und operativen Aufwand berücksichtigen. Alle Code-Beispiele sind dennoch auf das Wesentliche reduziert, um die Übersichtlichkeit zu gewährleisten, und enthalten beispielsweise absichtlich keine Tags. Die gesamte Infrastruktur wird als Infrastructure-as-Code mit Terraform konfiguriert, um die Reproduzierbarkeit zu erleichtern und als Basis für eigene Setups zu dienen.

Bitte beachte, dass die Bereitstellung von AWS-Infrastrukturressourcen Kosten verursachen kann. Unsere Terraform-basierten Tutorials helfen dir, alle Ressourcen mit einem einzigen Befehl terraform destroy zu entfernen, um ungewollte Kosten zu vermeiden.

Bei Gyden unterstützen wir Startups und KMUs beim Aufbau ihrer Cloud-Infrastruktur und begleiten sie dabei, technische Expertise zu erlangen. Häufig sind komplexe Setups wie containerbasierte Anwendungen in einer hochverfügbaren Umgebung die Ausgangsbedingungen, bei denen AWS ECS eine wichtige Rolle spielt, da es eine der flexibelsten Cloud-Computing-Technologien auf dem Markt ist.

Du findest das gesamte Tutorial auf GitHub, einschließlich des gesamten Quellcodes, um ein vollständig funktionsfähiges Setup auf Basis von ECS auf EC2 mit Terraform zu erstellen. Die einzige Voraussetzung ist eine Top-Level-Domain, die auf AWS gehostet wird (wie example.com). Weitere Informationen findest du in der README des Projekts. Klone das Repository mit:

ECS EC2 – Container-Orchestrierung ohne Kubernetes

Genau wie Fargate ist ECS EC2 eine Laufzeitumgebung für Dienste, die in Docker-Containern ausgeführt werden und maximale Flexibilität bei der Erstellung von Anwendungen jeder Grösse und Art bieten. Ein grosser Teil des Setups ist daher identisch mit der Variante mit Fargate, allerdings müssen wir uns hier selbst um die EC2-Instanzen (im ECS-Kontext auch Container Instances genannt) kümmern. Die Zielarchitektur sieht so aus:

Architektur-Übersicht für ECS auf EC2

Architektur-Übersicht für ECS auf EC2

In unserem Szenario schaffen wir die Grundlage für eine Multi-Tier-Architektur, konzentrieren uns jedoch auf eine Schicht, die als Präsentationsebene dient. Der Dienst ist über das Internet zugänglich und auf Hochverfügbarkeit (HA) ausgelegt. Dies wird erreicht, indem er in mehreren Availability Zones (AZ) bereitgestellt wird, während eine dynamische Skalierung durch eine Autoscaling TargetTracking Policy sichergestellt wird.

Ein Vergleich mit ECS Fargate

Grundsätzlich können wir mit sowohl EC2 als auch Fargate ein produktionsreifes Ergebnis erzielen. Der wesentliche Unterschied besteht darin, dass Fargate die serverlose Variante von ECS auf EC2 ist: Das gesamte Management der zugrundeliegenden Infrastruktur wird hier von AWS übernommen – und Amazon lässt sich dies natürlich bezahlen.

Folgende Punkte sollten beim Vergleich von EC2 und Fargate beachtet werden:

  • Ab einem bestimmten Punkt ist Fargate teurer als EC2. Den genauen Break-even zu bestimmen ist schwierig aufgrund der vielen Variablen, aber als Faustregel kann man sagen, dass Fargate günstiger ist, solange die Speicherauslastung im Cluster etwa 50 % nicht überschreitet. Bei höherer Auslastung wird Fargate jedoch teurer. AWS gibt in diesem Blogpost einen guten Einblick in die Preisstruktur.
  • Bei der Nutzung von Fargate muss sich das Team überhaupt nicht um das Management von Clustern, Instanzen, Containern, Task-Definitionen oder anderer Compute-Infrastruktur wie Autoscaling Groups kümmern. Je nach Teamstruktur und vorhandenem Wissen kann dies zu einer nicht unerheblichen Kosteneinsparung führen, jedoch ist dies sehr individuell je nach Unternehmen und schwer zu quantifizieren.
  • ECS auf EC2 bietet viel mehr Flexibilität bei der Auswahl bestimmter Instanztypen oder der Installation und dem Betrieb von Daemon-Tasks, Agents und ähnlichem auf den EC2-Instanzen. Fargate bietet diese Flexibilität nicht. Möchte man beispielsweise den Datadog Agent als eine Daemon-Task pro Instanz ausführen, ist dies mit Fargate nicht möglich.
  • Das Einbinden von EBS-Volumes ist mit Fargate nicht möglich. Ebenso wird der Einsatz von GPUs in Fargate-Clustern nicht unterstützt.

Zusammenfassend lässt sich sagen, dass ECS auf EC2 besonders dann die richtige Wahl ist, wenn das nötige Know-how sowie das Personal zum Verwalten der Infrastruktur vorhanden sind, eine hohe oder vollständige Flexibilität über die Infrastruktur benötigt wird und/oder spezielle Software wie Daemons als eigenständige Tasks ausgeführt werden muss. Man darf dabei nicht vergessen, dass man innerhalb von AWS natürlich auch mixen und einzelne Dienste auf Fargate und andere auf EC2-Instanzen betreiben kann. Übrigens werden Spot-Instanzen von beiden ECS-Varianten unterstützt.

ECS auf EC2 und CI/CD

Bei der Auswahl von Technologien und Strategien für ein cloudbasiertes Infrastruktur-Setup stellt sich immer zuerst die Frage, ob die Technologie einen modernen Entwicklungsansatz mit Continuous Integration und Continuous Deployment (CI/CD) sowie Infrastructure-as-Code unterstützt. Glücklicherweise ist Letzteres bei allen großen Cloud-Providern wie AWS, Google Cloud oder Microsoft Azure dank Terraform, Pulumi oder AWS' eigenem CloudFormation-Angebot immer gegeben. Für CI/CD müssen folgende Kriterien erfüllt sein:

  • Die gesamte Infrastruktur muss automatisch bereitgestellt und provisioniert werden können.
  • Es muss möglich sein, laufende Dienste durch eine neue Version im Rolling-Basis-Verfahren zu ersetzen, ohne den laufenden Betrieb zu unterbrechen.
  • Die Infrastruktur und alle Änderungen müssen versioniert und als Code verfügbar sein.
  • Wir möchten verschiedene Umgebungen wie development oder production mit einer Codebasis unterstützen.
  • Alle wichtigen Komponenten und Ressourcen wie Instanzen müssen mithilfe von Metriken überwachbar sein.

In diesem Artikel konzentrieren wir uns primär auf AWS-spezifische Ressourcen. Das Setup zur Automatisierung der Deployments, die Projektstruktur und die zugrunde liegenden Entwicklungsprinzipien betrachten wir in einem separaten Artikel, da das Thema ECS auf EC2 allein schon komplex genug ist.

Übersicht über die Hauptkomponenten

Die wichtigsten Komponenten in unserem Setup sind:

  • Route 53 für alle Routing- und Domaineinstellungen
  • Elastic Container Registry (ECR) für das Bereitstellen von Container-Images, die unseren Dienst enthalten
  • Certificate Manager zur Erstellung von SSL-Zertifikaten für den Application Load Balancer (ALB) und die CloudFront-Distribution
  • Application Load Balancer (ALB) und Target Groups/Listeners für die Weiterleitung von Anfragen an die verschiedenen Container-Instanzen
  • Autoscaling Group (ASG) und Autoscaling Policies für elastische Skalierung von EC2-Instanzen
  • Launch Template mit der Definition der EC2-Instanzen, die die Basis für ECS bilden
  • Bastion Host für den Zugriff per SSH zu Debugging-Zwecken auf die EC2-Instanzen
  • CloudFront Distribution für weltweit schnellen Zugriff auf unseren Dienst sowie die Möglichkeit, AWS WAF gegen SQL-Injections oder Ähnliches und AWS Shield gegen DDoS-Angriffe zu nutzen
  • Elastic Container Service (ECS) Cluster, ECS Service und ECS Tasks zum Betrieb unseres Dienstes
  • Security Groups für die detaillierte Konfiguration von netzwerkbasierten Firewall-Regeln für EC2 und ALB
  • VPC für alle Subnetze und das Internet Gateway für ein- und ausgehenden Netzwerkzugang zu den öffentlichen Subnetzen
  • Private Subnets inklusive NAT Gateway für den ECS-Dienst
  • Public Subnets für den ALB

Wie du sehen wirst, verwenden wir konsequent eindeutige Namen mit der Umgebung (z. B. dev) für die Benennung von Ressourcen. Dadurch können wir Ressourcen aus mehreren Umgebungen (wie staging oder pre-production) im selben AWS-Konto in einem realistischen Szenario bereitstellen.

Die gesamte Codebasis mit einem funktionierenden lokalen Setup findest du in unserem GitHub-Repository.

Fangen wir mit einigen grundlegenden Komponenten an, nämlich einer Hosted Zone, den Zertifikaten und einem Elastic Container Registry (ECR).

Route 53, ECR und Certificate Manager

Für unser Beispiel nehmen wir an, dass unser Dienst unter einer Subdomain laufen wird, z. B. https://service.example.com. Dafür müssen wir eine neue Hosted Zone in Route 53 mit Nameserver-Einträgen (NS) in der Hosted Zone der Top-Level-Domain example.com erstellen.

Als Nächstes benötigen wir zwei Zertifikate: eines für den ALB und eines für CloudFront. Wichtig ist dabei Folgendes: Während das Zertifikat für den ALB in der Region des ALB selbst erstellt werden muss, müssen CloudFront-Zertifikate immer in der Region us-east-1 erstellt werden. Das bedeutet, wenn dein Setup grundsätzlich in der Region North Virginia läuft, kannst du mit einem einzigen Zertifikat arbeiten. Läuft es jedoch in einer anderen Region, benötigst du zwei separate Zertifikate.

Ähnlich wie bei der Einrichtung mit Fargate benötigen wir auch hier eine Container-Registry (eine Art Repository für Docker-Images), in der wir die neuesten Images unseres Presentation-Tier-Services speichern können. Neben dem AWS-eigenen Service ECR sind auch Dienste wie Docker Hub oder das GitHub Container Registry passende Alternativen.

Wir aktivieren ausserdem direkt das Scannen von Images auf bekannte Sicherheitslücken. Der Vollständigkeit halber (und um Kosten zu sparen) sollte auch eine Lifecycle-Policy erstellt werden, um alte oder nicht getaggte Images zu löschen. Dieser Schritt ist in unserem Beispiel nicht dargestellt. In einer Produktionsumgebung sollte force_delete deaktiviert sein, da dies die ECR-Ressource einschliesslich aller Images im Repository löscht, wenn ein terraform destroy-Befehl oder eine Änderung, die einen Ersatz erzwingt, ausgeführt wird.

Die Netzwerkkonfiguration

Die Grundlage für jede internetbasierte Anwendung in AWS ist eine Virtual Private Cloud (VPC) in Kombination mit Subnetzen und Ressourcen wie Internet-Gateways, NAT-Gateways und möglicherweise anderen Komponenten wie Site-to-Site-VPNs, Transit-Gateways und weiteren.

Im nächsten Schritt kümmern wir uns um diese Netzwerkomponenten. Wir starten mit der VPC und einem Internet-Gateway:

Für Hochverfügbarkeit müssen wir unsere privaten und öffentlichen Subnetze in mehreren (mindestens zwei) Availability Zones (AZ) bereitstellen. Dafür können wir die folgende data-Ressource von Terraform verwenden:

Öffentliche Subnetze

Wir erstellen nun in jeder verfügbaren Availability Zone (AZ) in der ausgewählten AWS-Region ein öffentliches Subnetz, eine Routentabelle und eine Route für ausgehenden Zugriff auf das Internet. Die öffentlichen Subnetze werden für den Application Load Balancer (ALB) und den Bastion Host verwendet, jedoch nicht für die EC2-Instanzen, die Teil des ECS-Clusters sind.

Private Subnetze

Für die privaten Subnetze führen wir nahezu die gleichen Schritte wie für die öffentlichen Subnetze durch, jedoch müssen wir hier ein NAT-Gateway hinzufügen, da ansonsten kein Zugriff auf das Internet möglich ist. Entsprechend muss eine Route zum Internet (CIDR-Block 0.0.0.0/0) zu den Routentabellen hinzugefügt werden. Alle unsere Container-Instanzen werden innerhalb der privaten Subnetze ohne direkten Zugriff aus dem Internet platziert.

Die Compute-Schicht auf EC2

Im Gegensatz zu ECS auf Fargate sind wir bei ECS auf EC2 selbst für den Betrieb und die Einrichtung der EC2-Instanzen verantwortlich. Das gibt uns zwar mehr Flexibilität, führt jedoch auch zu einem höheren operativen Aufwand. Um diesen so gering wie möglich zu halten, nutzen wir eine AWS Launch Template, um die EC2-Instanzen in unserem ECS-Cluster zu erstellen.

Zusätzlich benötigen wir ein EC2 Key Pair, um Zugriff auf die EC2-Instanzen zu erhalten. Du kannst hierfür deinen bestehenden, kompatiblen SSH-Schlüssel verwenden oder einen neuen erstellen:

Wir werden diesen SSH-Schlüssel später verwenden, um uns über den Bastion Host mit einer unserer privaten EC2-Instanzen zu verbinden. Im nächsten Schritt erstellen wir eine Key Pair Ressource:

Als nächstes erstellen wir das Launch Template. Hier gibt es eine kleine Stolperfalle: Beim Auswählen eines geeigneten Amazon Machine Image (AMI) solltest du darauf achten, ein ECS-optimiertes Image zu verwenden, auf dem der ECS-Agent installiert ist. In unserem Beispiel nutzen wir ein AMI der Gruppe amzn2-ami-ecs-hvm-*-x86_64-ebs.

Falls dies nicht der Fall ist, kannst du den ECS-Agent manuell installieren, aber ohne diesen kannst du keine EC2-Instanz in einem ECS-Cluster verwenden.

Um die EC2-Instanzen nach dem Start als Teil des ECS-Clusters nutzen zu können, müssen wir den Clusternamen konfigurieren. Dazu wird der Name in die ECS-Konfigurationsdatei /etc/ecs/ecs.config geschrieben.

Dieser Schritt erfolgt im Rahmen des User-Data-Skripts user_data.sh, das beim Start einer EC2-Instanz ausgeführt wird. Denke daran, dass User-Data-Skripts bei einem Neustart der Instanz nicht erneut ausgeführt werden.

Im letzten Schritt müssen wir der Launch Template und damit den Container-Instanzen eine IAM-Rolle mit den erforderlichen Berechtigungen zuweisen:

Als Nächstes erstellen wir das ECS-Cluster, den ECS-Service und die ECS-Task-Definition. Wir beginnen mit dem ECS-Cluster:

Danach folgt der ECS-Service mit der entsprechenden IAM-Rolle. Unter anderem können hier wichtige Konfigurationen vorgenommen werden, wie die ECS-Tasks auf den einzelnen Container-Instanzen verteilt werden sollen. Dies ist besonders wichtig im Hinblick auf Hochverfügbarkeit. In diesem Fall werden die Aufgaben so konfiguriert, dass die Anzahl der ECS-Tasks gleichmässig auf die EC2-Instanzen in den verschiedenen Availability Zones verteilt wird. Wir verwenden die binpack-Methode, um den effizientesten Raumnutzungsmechanismus auf den Container-Instanzen zu gewährleisten.

Es ist wichtig, den Parameter desired_count bei Änderungen in einem Deployment-Setup, das für hohe Leistung mit CI/CD und häufigen Commits (mehrmals pro Stunde) ausgelegt ist, zu ignorieren. Unsere Auto-Scaling-Einrichtung entscheidet unabhängig, wie viele Ressourcen und ECS-Tasks gleichzeitig ausgeführt werden müssen, um die Zielverfolgungsrichtlinie (Target Tracking Policy) zu erfüllen. Wenn desired_count immer wieder neu bereitgestellt würde, würden wir die Kapazitätsplanung jedes Mal umgehen und sie sozusagen zurücksetzen.

Nun können wir die IAM-Rolle für den ECS-Service erstellen. Diese Rolle wird von ECS in unserem Auftrag übernommen, um das ECS-Cluster zu verwalten.

Und schliesslich die ECS-Task-Definition, die festlegt, welche Einstellungen unser Docker-Container verwenden wird, um mit unserem Service zu laufen. Hier ist die folgende Zeile wichtig:

Die Variable ${var.hash} stammt aus dem zugrunde liegenden CI/CD-System (z. B. Jenkins, CircleCI) und besteht aus dem Git-Commit-Hash, der für jedes Commit unterschiedlich ist. Dieser Wert wird bei jeder Bereitstellung als Variable an Terraform übergeben (z. B. über die Umgebungsvariable TF_VAR_hash). Dies stellt sicher, dass mit jeder Bereitstellung eine neue Version der ECS-Task erstellt und bereitgestellt wird – ein wesentlicher Bestandteil eines trunk-basierten Entwicklungsprozesses, bei dem jede Änderung direkt integriert und gemäß den Regeln von Continuous Integration und Continuous Deployment (CI/CD) bereitgestellt wird.

Nun müssen wir sicherstellen, dass unsere Logging-Gruppe tatsächlich existiert. Diese Logging-Gruppe wird auch für das Anwendungs-Logging innerhalb unseres Services verwendet (zum Beispiel eine Python-App, die eine Anweisung wie die folgende nutzt):

Die Definition der Log-Gruppe enthält eine Log-Retention-Periode, die ältere Logs nach einer bestimmten Zeit löscht.

Für die ECS-Task erstellen wir die beiden Rollen Nexgeneerz_ECS_TaskExecutionRole und Nexgeneerz_ECS_TaskIAMRole, die ich später im Detail erläutern werde.

Mit diesem Schritt haben wir den Rechenbereich abgeschlossen und können uns der Konfiguration des Autoscalings auf ECS widmen.

Autoscaling auf ECS

Zunächst werfen wir einen Blick auf unser Setup. Auf der einen Seite haben wir eine Gruppe von x EC2-Instanzen, die zu einem ECS-Cluster gehören und von einem oder mehreren ECS-Services genutzt werden (in unserem Beispiel von einem Service). Die EC2-Instanzen werden von einer Autoscaling-Gruppe bereitgestellt und im ECS-Cluster registriert. Der ECS-Cluster verwaltet wiederum unsere Container (oder ECS-Tasks), die unseren Service enthalten. Somit haben wir auch die Anzahl der ECS-Tasks, die wir in der Autoscaling-Gruppe einbeziehen können.

Dies führt bereits zu einem Teil der Komplexität bei ECS auf EC2, die AWS bei EC2 auf Fargate von uns nimmt: Wir müssen selbst herausfinden, welche Strategie wir verwenden möchten, um eine flexible Skalierung (Scale-out und Scale-in) der Services zu erreichen. Es gibt hier verschiedene Optionen, aber glücklicherweise hat AWS diese Aufgabe durch die Einführung von ECS Capacity Providers und ECS Service Autoscaling deutlich vereinfacht.

Wir konfigurieren das Autoscaling auf zwei verschiedenen Ebenen mithilfe von Target Tracking Policies:

  • Capacity Provider mit Target Tracking Policy für eine Zielkapazität
  • Service Autoscaling auf ECS-Service-Ebene mit Target Tracking Policy für CPU- und Speichernutzung

Capacity Providers

Ein AWS Capacity Provider fungiert als Verbindung zwischen dem ECS-Cluster und der Autoscaling-Gruppe und ist mit beiden Ressourcen verknüpft. Prinzipiell kann jedes ECS-Cluster mehrere Capacity Providers und somit verschiedene Autoscaling-Gruppen verwenden. Dies ermöglicht es, verschiedene Infrastrukturtypen wie On-Demand-Instanzen und Spot-Instanzen gleichzeitig im Cluster zu nutzen.

Capacity Providers berechnen die benötigte Infrastruktur für ECS-Task-Container und Container-Instanzen (auch EC2-Instanzen genannt) basierend auf Variablen wie virtueller CPU oder Arbeitsspeicher. Sie kümmern sich um das Skalieren (Scale-out und Scale-in) beider Komponenten nach Bedarf mithilfe einer Target Tracking Policy mit einem Zielwert für die CPU- und/oder Speichernutzung. Ein Zielwert von 50 % für die CPU-Nutzung bedeutet beispielsweise, dass der Capacity Provider immer versucht, die Anzahl der EC2-Instanzen so zu balancieren, dass dieser Wert nicht überschritten (Scale-out) oder erheblich unterschritten (Scale-in) wird. Diese nachfragesteuerbare, elastische Skalierung von Infrastruktur macht Cloud-Anbieter wie AWS oder Google Cloud zu äußerst nützlichen Partnern, da man als Nutzer nur für die Infrastruktur bezahlt, die tatsächlich genutzt wird. In einem On-Demand-Rechenzentrum würde nicht genutzte Rechenleistung ungenutzt herumliegen und trotzdem Kosten verursachen.

Ein sehr detaillierter Einblick in Capacity Providers ist in diesem Artikel von Annie Holladay zu finden. Eine allgemeine Übersicht über das komplexe Thema Autoscaling auf AWS wird von Nathan Peck bereitgestellt, während Durai Selvan speziell das Thema Scaling auf AWS ECS behandelt.

Unsere Capacity Provider-Konfiguration ist wie folgt definiert:

Hier definieren maximum_scaling_step_size und minimum_scaling_step_size, um wie viele EC2-Instanzen der Capacity Provider die Anzahl der Container-Instanzen während eines Scale-out oder Scale-in gleichzeitig erhöhen oder verringern darf. managed_termination_protection verhindert, dass EC2-Instanzen, auf denen noch andere Tasks laufen, beendet werden.

Service Autoscaling auf ECS

Das Service Autoscaling kümmert sich um die elastische Skalierung von Containern (ECS-Tasks) und funktioniert in unserem Setup ebenfalls über Target Tracking für CPU- und Speichernutzung.

In der Ressource aws_appautoscaling_target definieren wir die minimale und maximale Anzahl von Tasks, die gleichzeitig laufen dürfen, ähnlich wie im Capacity Provider. Dies hilft uns, trotz Skalierbarkeit die Kosten im Griff zu behalten. min_capacity ist ein wichtiger Wert, der in unserem Setup auf mindestens 2 gesetzt wird, um Hochverfügbarkeit zu gewährleisten. Da wir aws_ecs_service mit der spread Placement Strategy konfiguriert haben, wird sichergestellt, dass jeder der beiden Tasks in einer unterschiedlichen Availability Zone läuft. Im Falle eines Ausfalls einer AZ ist somit ein Backup vorhanden, und der Service kann ohne Unterbrechung gewährleistet werden.

Wir verwenden ECSServiceAverageCPUUtilization und ECSServiceAverageMemoryUtilization als Metriken, deren Daten darüber entscheiden, ob ein Scale-out oder Scale-in ausgelöst werden soll.

Autoscaling-Gruppe

Das letzte fehlende Element, um ECS-Cluster, Capacity Providers und Launch Template zu verknüpfen, ist die Autoscaling-Gruppe (ASG). Auch hier definieren wir, ähnlich wie bei den ECS-Tasks, die minimale und maximale Anzahl von EC2-Instanzen, die erstellt werden dürfen, um unkontrolliertes Skalieren zu vermeiden. Die Autoscaling-Gruppe nutzt unser bereits konfiguriertes Launch Template, um neue Instanzen zu starten.

Der instance_refresh-Block hat eine wichtige Aufgabe, da er es uns ermöglicht, die Aufwärmzeit zu konfigurieren, mit der neue EC2-Instanzen verfügbar sein sollen, um zu lange Startzeiten zu vermeiden. Mit enabled_metrics wird definiert, welche Metriken die Autoscaling-Gruppe (ASG) bereitstellen soll. Diese Metriken stehen dann in CloudWatch zur Verfügung. protect_from_scale_in muss auf true gesetzt werden, da wir managed_termination_protection im Capacity Provider aktiviert haben.

Ein wichtiges Detail ist, dass die EC2-Instanzen innerhalb der Autoscaling-Gruppe alle in den Private Subnets erstellt werden. Dadurch können wir das Sicherheitsniveau erheblich erhöhen, da die Firewall-Einstellungen in den Security Groups nur kontrollierten Datenverkehr in der Reihenfolge CloudFront Distribution -> ALB -> Autoscaling Group zulassen.

Application Load Balancer

Der Application Load Balancer (ALB) ist Teil von Amazons Elastic Load Balancing (ELB) und übernimmt die Aufgabe des Load Balancing, also der gleichzeitigen Verteilung von Anfragen auf die verfügbaren Container-Instanzen und ECS-Tasks. Im Gegensatz zu den Container-Instanzen läuft der ALB in den Public Subnets. Er ist eine hochverfügbare Komponente und durch unser Setup mit mehreren AZs redundant ausgelegt.

Der ALB empfängt eingehenden Datenverkehr über ALB-Listener. Wir definieren einen HTTPS-Listener, was bedeutet, dass wir nur Datenverkehr auf Port 443 (HTTPS) akzeptieren können. Die automatische Weiterleitung von HTTP auf HTTPS wird später in der CloudFront-Distribution konfiguriert.

HTTPS Listener und Target Group

Die Standardaktion im HTTPS Listener des Application Load Balancers blockiert alle Anfragen mit dem Statuscode 403 (Access Denied). Im weiteren Verlauf fügen wir nun eine Listener-Regel hinzu, die den Zugriff nur mit einem gültigen Custom Origin Header erlaubt. Als Zertifikat, das für HTTPS benötigt wird, verwenden wir unser zu Beginn erstelltes Zertifikat alb_certificate.

Die ALB Listener-Regel verknüpft eingehende Anfragen, die mit dem korrekten Custom Origin Header gesendet werden, mit unserer Target Group.

Die Target Group ist die Ausgabe des ALB, während der ALB Listener den Eingang darstellt. Der Datenverkehr gelangt in den Listener und wird an die entsprechende Target Group weitergeleitet, die ihn dann an die entsprechenden Backend-Ressourcen (in unserem Fall Container-Instanzen) routet. Ab diesem Punkt wird der Datenverkehr über HTTP auf Port 80 weitergeleitet. Da wir keine durchgehend verschlüsselte Lösung implementieren und uns jetzt innerhalb der Private Subnets befinden, ist dies unproblematisch.

Der Block health_check definiert, welche URL das ALB verwendet, um zu entscheiden, ob das Ziel (unsere auf den Container-Instanzen ausgeführten ECS-Tasks) gesund ist. Mit matcher und path können ein oder mehrere HTTP-Statuscodes und ein spezifischer URL-Pfad für den Healthcheck konfiguriert werden. healthy_threshold und unhealthy_threshold legen fest, nach wie vielen (fehlgeschlagenen) Healthchecks ein Ziel als gesund oder ungesund bewertet wird. Diese Healthchecks werden in Intervallen durchgeführt, die in interval definiert sind.

Je nach Anwendungslogik sollte der Block stickiness definiert werden, um sicherzustellen, dass Sticky Sessions verwendet werden. Auf diese Weise werden Besucher immer zum selben Ziel geleitet und ihre Sitzung bleibt erhalten. Dieser Ansatz kann jedoch problematisch sein, insbesondere bei häufigen Deployments und damit häufig wechselnden Zielen. Es sollte in der Anwendungsarchitektur berücksichtigt werden, um effiziente Entwicklungsmethoden wie trunk-based Development zu ermöglichen.

Die Verknüpfung der Target Group mit unserem ECS-Service erfolgt direkt in der Ressource aws_ecs_service. Im Block load_balancer wird die ARN der Target Group verlinkt und die ECS-Tasks werden als Ziele registriert.

Security Groups

Als Nächstes kümmern wir uns um die erforderlichen Security Groups (SG) für ALB und EC2-Instanzen. Die SG für die EC2-Instanzen erlaubt sämtlichen ausgehenden Datenverkehr (oft erforderlich, um externe API-Aufrufe zu tätigen oder für Paketmanager wie pip oder npm). Eingehender Datenverkehr ist ausschließlich auf Port 22 (SSH) vom Bastion Host erlaubt, den wir später erstellen. Außerdem erlauben wir die sogenannten Ephemeral Ports 102465535, die von der Security Group des ALB kommen.

Das ALB erlaubt ebenfalls sämtlichen ausgehenden Datenverkehr. Eingehend begrenzen wir den Zugriff erneut auf bekannte AWS-CIDR-Block-Bereiche, die wir über die Ressource aws_ec2_managed_prefix_list in Form von Managed Prefix Lists abfragen können. Dies stellt eine zusätzliche Sicherheitsebene zum Custom Origin Header dar.

CloudFront Distribution

Als letzte Komponente nutzen wir eine CloudFront Distribution, um weltweit schnellen und latenzarmen Zugriff auf unseren Service zu ermöglichen (ein Multi-Region-Setup würde dieses Szenario noch weiter verbessern). Gleichzeitig profitieren wir von den Web Application Firewall Rules (WAF) sowie von AWS Shield, das standardmäßig aktiviert ist. WAF dient der Erkennung und Verhinderung bösartiger Angriffe wie SQL-Injection, während AWS Shield eine erste Sicherheitsstufe gegen DDoS-Angriffe bietet. AWS Shield kann bei Bedarf auf das kostenpflichtige AWS Shield Advanced erweitert werden.

CloudFront ist über target_origin_id mit unserem ALB verknüpft. Mithilfe der Konfiguration redirect-to-https stellen wir sicher, dass aller eingehender Traffic für den Benutzer stets auf HTTPS umgeleitet wird. Wenn wir als Benutzer beispielsweise http://service.example.com eingeben, werden wir automatisch zu https://service.example.com weitergeleitet.

Im custom_header konfigurieren wir unseren Custom Origin Header, den wir bereits in unserem ALB Listener vorbereitet haben. Als acm_certificate_arn nutzen wir hier das speziell in der Region us-east-1 erstellte Zertifikat.

Bastion Host

Einer der Vorteile von ECS auf EC2 im Vergleich zu ECS Fargate ist, dass wir die volle Kontrolle über die zugrunde liegende Compute-Schicht (EC2-Instanzen) haben. Dies kann unter anderem auch beim Debuggen von Docker-Containern hilfreich sein, die nicht korrekt starten. Dafür benötigen wir Zugriff auf die EC2-Instanzen, was über SSH möglich ist. Da die Container-Instanzen jedoch in einem privaten Subnetz laufen und keine öffentliche IP-Adresse haben, müssen wir eine andere Methode finden, um eine sichere Verbindung herzustellen.

Mit einem Bastion Host (manchmal auch Jump Host genannt), einer separaten kleinen EC2-Instanz, die in einem unserer öffentlichen Subnetze läuft und nur Zugriff über SSH auf Port 22 zulässt, können Benutzer mit dem entsprechenden privaten Schlüssel über SSH Agent Forwarding eine Verbindung zu den Container-Instanzen herstellen. Alternativ könnte auch AWS Instance Connect verwendet werden, um eine Terminal-Sitzung auf der Container-Instanz direkt über die AWS-Konsole ohne explizite Verwendung des SSH-Schlüssels zu starten.

DNS-Einstellungen mit Route 53

Um unser gesamtes Setup abzurunden, benötigen wir noch die finalen DNS-Einstellungen, die wir mit Route 53 konfigurieren. In unserem Beispiel nehmen wir an, dass unser Service unter einem Subdomain wie service.example.com läuft und für verschiedene Entwicklungsumgebungen im dev-Umfeld unter dev.service.example.com zugänglich sein soll. Daher benötigen wir eine neue Route 53 Hosted Zone für diese Subdomain, die entsprechenden Nameserver (NS)-Einträge in der übergeordneten Zone und einen A-Record, der auf unsere CloudFront-Distribution als Einstiegspunkt für den Service verweist.

IAM-Rollen in AWS ECS

AWS ECS verwendet insgesamt vier verschiedene IAM-Rollen, die alle im Kontext von EC2-Instances, ECS-Service und ECS-Tasks verwendet werden. Diese Rollen können etwas verwirrend sein, wenn nicht klar ist, welche Rolle welche Aufgaben übernimmt.

Wir verwenden die folgenden IAM-Rollen:

  • Die Nexgeneerz_EC2_InstanceRole im Launch Template: Diese Rolle wird von jeder einzelnen EC2-Instance angenommen, die gestartet wird. Da wir mit AWS Elastic Container Services (ECS) arbeiten, verwenden wir hier die eigene Service-Rolle von AWS, AmazonEC2ContainerServiceforEC2Role. Diese Rolle gewährt zum Beispiel die Berechtigung ecs:RegisterContainerInstance, die vom ECS Service-Agenten genutzt wird, der auf der EC2-Instance läuft, um sich als neu gestartete Container-Instance im ECS-Cluster zu registrieren. Eine Übersicht über alle gewährten Berechtigungen ist hier zu finden. Zusammengefasst: Die Nexgeneerz_EC2_InstanceRole wird vom Principal EC2-Instance übernommen, um Aufgaben wie das (De-)Registrieren von Container-Instances auszuführen.
  • Die Nexgeneerz_ECS_ServiceRole, die vom ECS-Service verwendet wird: Hier wird eine service-gebundene Rolle mit einer ähnlichen Richtlinie wie AmazonECSServiceRolePolicy verwendet, die es ECS ermöglicht, das Cluster zu verwalten. In der AWS-Konsole würde diese Rolle automatisch für uns erstellt werden, aber im Fall von Infrastructure-as-Code verwalten wir dies selbst und verwenden eine separate Rolle, um Konflikte beim Einsatz mehrerer ECS-Cluster und ECS-Services zu vermeiden.
  • In der Ressource aws_ecs_task_definition werden zwei Rollen erwartet. Die execution_role_arn ist die Nexgeneerz_ECS_TaskExecutionRole, die die AmazonECSTaskExecutionRolePolicy-Richtlinie übernimmt. Die Task Execution Role gewährt dem ECS-Agenten die erforderlichen Rechte, um zum Beispiel Logstreams zu schreiben. Je nach Anforderung kann es notwendig sein, eine separate Inline-Richtlinie zu definieren.
  • Die Rolle Nexgeneerz_ECS_TaskIAMRole wird vom ausgeführten ECS-Task selbst übernommen. Sie kann erforderlich sein, wenn der Task Zugriff auf zusätzliche AWS-Dienste benötigt (was in unserem Beispiel nicht der Fall ist, daher existiert diese Rolle nur als leeres Containerobjekt und erhält keine Berechtigungen).

Verbindung zu den EC2-Instances

Einer der Vorteile von ECS EC2 gegenüber ECS Fargate ist der uneingeschränkte Zugriff auf die EC2-Instances, die wir über das Launch Template steuern, einschließlich der Security Groups und SSH-Schlüssel. Für Debugging-Zwecke kann es manchmal hilfreich sein, eine Verbindung zu diesen EC2-Instances über SSH herzustellen. Dafür haben wir die entsprechenden Sicherheitsgruppenregeln hinterlegt und den Bastion-Host erstellt, da die EC2-Instances in einem privaten Subnetz laufen, das keinen direkten Zugriff aus dem Internet hat.

Um eine Verbindung zu einer der Instanzen herzustellen, müssen wir die private IP-Adresse dieser EC2-Instance in der AWS-Konsole ermitteln (z. B. 192.173.98.20). Wir benötigen auch die öffentliche IP-Adresse des Bastion-Hosts, sagen wir, sie lautet 173.198.20.89. Da wir Agent-Forwarding über SSH verwenden, müssen wir die folgenden Befehle ausführen, wobei wir den Standard-EC2-Instanzbenutzer ec2-user verwenden:

Sobald du im Bastion Host eingeloggt bist, kannst du von dort aus die Verbindung zur privaten EC2-Instanz fortsetzen:

Nach dem erfolgreichen Einloggen können wir weitere Aktionen durchführen, wie zum Beispiel das Debuggen, warum ein bestimmter Docker-Container nicht startet.

Was fehlt noch?

Obwohl wir bereits viele der notwendigen Komponenten für eine produktionsfähige Infrastruktur in unserem Setup behandelt haben, gibt es noch einige Konfigurationen, die wir aus Gründen der Klarheit und Verständlichkeit in diesem Tutorial weggelassen haben. Dazu gehören die folgenden Komponenten:

  • Konfiguration von AWS Web Application Firewall (WAF)-Regeln für CloudFront.
  • Möglicherweise die Konfiguration von ACL auf Netzwerkebene.
  • Einschränkung des IP-Bereichs für den Bastion Host auf spezifische CIDR-Blöcke, wobei der Zugriff über AWS EC2 Instance Connect in der AWS-Konsole hier aufgrund einer besseren Zugriffskontrolle bevorzugt wird.

Trotzdem bietet dieser Artikel bereits einen umfassenden Einblick in die Konfiguration, Bereitstellung und Skalierung von ECS auf EC2 für Unternehmen, die ihre Anwendungen in Docker-Containern in der AWS-Cloud ausführen möchten.

Fazit

ECS auf EC2 ist ein äußerst leistungsfähiges Tool, um komplexe Infrastrukturen ohne nennenswerte Einschränkungen zu erstellen. Der Aufbau und das Management der Infrastruktur erfordert ein solides Verständnis von Techniken wie Autoscaling sowie von Aspekten wie Lastenverteilung, Sicherheitsgruppen und Netzwerkmanagement. In puncto Flexibilität ist ECS auf EC2 fast auf Augenhöhe mit Kubernetes und kann in nahezu jede Richtung erweitert werden.

Dennoch sollte man den betrieblichen Aufwand, der mit dieser Flexibilität einhergeht, nicht unterschätzen. Insbesondere kleinere Teams könnten hier an ihre Grenzen stoßen, wenn sie sich dieses Wissen zunächst mühsam aneignen müssen und dabei den operativen Teil vernachlässigen. Mit unserem Angebot „Cloud Computing für Startups“ unterstützen wir solche Unternehmen besonders dabei, von flexibler und hochverfügbarer Infrastruktur zu profitieren, das nötige Wissen zuverlässig aufzubauen und dann die weitere Entwicklung eigenverantwortlich voranzutreiben. Infrastruktur in der Cloud wird zukünftig immer mehr zum Standard, und Software-Engineering-Teams tun gut daran, sich rechtzeitig die nötigen Skills anzueignen, um eine kosteneffiziente Produktentwicklung mit schneller Markteinführung sicherzustellen.

Puh, das war ein großes Thema! 😅

Hoffentlich konntest du in unserem Artikel über Amazon ECS auf EC2 mit Terraform etwas lernen und es auf deinen eigenen Anwendungsfall anwenden.

Unser Ziel bei Gyden ist es, Wissen mit anderen in der Tech-Community zu teilen und gemeinsam zu lernen. Denn nur so können wir die Komplexität, die mit solchen Technologien einhergeht, weiter verringern.

Wenn dir dieser Beitrag gefallen hat, teile ihn mit deinen Freunden und Kollegen. Gemeinsam können wir mehr Menschen helfen, über Cloud Computing und Amazon ECS zu lernen und zu wachsen. Du kannst uns auch auf LinkedIn folgen, um über neue Artikel wie diesen auf dem Laufenden zu bleiben. 🚀