AWS Fargate: Container-Orchestrierung ohne Cluster

Seite 2: Eine Fargate Bastion

Inhaltsverzeichnis

Das vollständige und vollautomatisierte Beispiel für die Fargate Bastion ist auf GitHub verfügbar. Es enthält ein Skript zur automatischen Installation in der AWS Cloud, ein weiteres zum Hochladen von SSH-Keys der Bastion-Nutzer und eines, um nach Bedarf Bastion Tasks zu starten. In diesem Artikel sind Teile dieser Automatisierung genauer erklärt, die auch zum manuellen Reproduzieren per AWS Command Line Interface und AWS CloudFormation befähigen sollen. CloudFormation ist der AWS-Ansatz, um Infrastrukturkomponenten in der Cloud zu erstellen und zu verändern. Das Arbeiten mit einem solchen Infrastructure-as-Code-Werkzeug spart Zeit, vereinfacht das konsistente Erstellen von Cloud-Ressourcen und reduziert somit die Differenzen zwischen den erzeugten Umgebungen.

Abbildung 1: Das Bastion-Beispiel besteht aus mehreren miteinander verbunden Komponenten. Das Fargate-Cluster ist nur ein Teil davon und der Bastion Task kann mehrfach oder gar nicht vorhanden sein – je nachdem wie viele Nutzer den Task gerade laufen lassen.

Das Funktionsprinzip der Fargate Bastion lässt sich mit Abbildung 1 erklären. Eine Task Definition beschreibt die Container, deren Netzwerkanbindung und Berechtigungen, die eine laufende Bastion-Instanz benötigt. Möchte man sich mit seinen Cloud-Ressourcen per SSH verbinden, startet man das entsprechende Skript. Beim darauffolgenden Start-Prozess wird die Task-Definition für die Instanziierung eines Bastion Task innerhalb eines Fargate-Clusters genutzt. Der laufende Docker-Container wird mit einem Elastic Network Interface verbunden, das über eine öffentliche IP-Adresse verfügt. Sobald die Initialisierung der Laufzeitumgebung abgeschlossen ist, lädt der Container selbstständig aus einem AWS Simple Storage Service (S3) Bucket die abgelegten Host-Schlüssel und individuellen öffentlichen Schlüssel der Nutzer herunter. Hierdurch können sich die Nutzer dann per SSH mit ihren privaten Schlüsseln authentifizieren und mit dem Container verbinden. Um Zugriff auf die im Bucket abgelegten Schlüssel zu erhalten, müssen Entwickler dem Task über eine Rolle die entsprechenden Berechtigungen zuweisen.

BastionConfigBucket:
  Type: 'AWS::S3::Bucket'
  Properties:
    BucketName: !Sub ${AWS::StackName}-keyfiles-${AWS::AccountId}

FargateCluster:
  Type: "AWS::ECS::Cluster"
  Properties:
    ClusterName: !Sub ${AWS::StackName}-cluster

BastionLogGroup:
  Type: "AWS::Logs::LogGroup"
  Properties:
    LogGroupName: !Sub ${AWS::StackName}-bastion
    RetentionInDays: 1

Listing 1: Ein Fargate-Cluster, eine Log-Gruppe und ein S3 Bucket lassen sich mit nur wenigen Zeilen CloudFormation beschreiben.

Die Infrastruktur für die Fargate Bastion wird vollständig über CloudFormation erzeugt. CloudFormation-typisch bestehen Ressourcen aus einem Typ und den Eigenschaften. Für ein Fargate-Cluster oder S3 Bucket reicht es, jeweils den Typ und bei Bedarf einen Namen anzugeben. Für das S3 Bucket verwenden wir zusätzlich die Accountnummer, um Namenskollisionen mit S3 Buckets anderer AWS-Kunden zu vermeiden. Zusätzlich wird eine CloudWatch LogGroup erstellt, um die Logs der gestarteten Bastion-Container zu aggregieren und zu indizieren.

TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: bastion
    Cpu: 256
    Memory: 512
    NetworkMode: awsvpc
    RequiresCompatibilities:
      - FARGATE
    ExecutionRoleArn: !Ref TaskExecutionRole
    TaskRoleArn: !Ref TaskRole
    ContainerDefinitions:
      - Name: bastion
        Cpu: 256
        Memory: 512
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BastionImageRepository}:latest
        PortMappings:
          - ContainerPort: 42
            HostPort: 42
        Environment:
          - Name: S3_BUCKET_NAME
            Value: !Sub ${AWS::StackName}-keyfiles-${AWS::AccountId}
        Command:
          - '/bin/sh'
          - '/usr/local/bin/entrypoint.sh'
          - '-p'
          - '42'
        LogConfiguration:
          LogDriver: awslogs
          Options:
            awslogs-group: !Ref BastionLogGroup
            awslogs-region: !Ref 'AWS::Region'
            awslogs-stream-prefix: 'ssh'

Listing 2: Die Taskdefinition beschreibt einen einzelnen Bastion-Container, seine Ressourcen-Zuweisung, Netzwerkanbindung, Berechtigungen, Konfiguration und Startparameter.

Bei dem Task-Definition-Typ sind zuerst die Eigenschaften "CPU" und "Memory" hervorzuheben, die angeben, in welchem Umfang der später laufende Task Ressourcen benötigt. Für den CPU-Wert sind das Anteile an CPU-Kernen. Der im Beispiel verwendete Wert "256" sagt aus, dass dieser Container ein Viertel eines virtuellen CPU-Kerns verwenden darf. Der aktuelle Maximalwert "4096" hingegen bedeutet entsprechend vier CPU-Kerne. Sorgfältige Überlegungen erfordert der Memory-Wert, der den reservierten Arbeitsspeicher für den Task beschreibt. Andernfalls wird bei Überschreiten dieses Werts durch Speicherallokationen des Containers der Task mit sofortiger Wirkung terminiert. Dies ist besonders einleuchtend vor dem Hintergrund, dass beide Werte auch den Preis eines Fargate-Containers bestimmen.

Der eingestellte Netzwerkmodus ist der einzig zulässige Modus bei Fargate und bezweckt, dass der Container nicht die Netzwerkanbindung des Docker-Hosts verwendet, sondern ein eigenes Elastic Network Interface erhält. Ein solches Interface ist vergleichbar mit einer virtuellen Netzwerkkarte, die an das eigene virtuelle Cloud-Netzwerk angeschlossen wurde. Hierdurch ist es möglich, einen einzelnen Task netzwerktechnisch wie eine vollwertige EC2-Instanz zu verwenden. Beispielsweise ermöglicht dies, jenem Task eine öffentliche IP-Adresse zuzuweisen oder feingranular den erlaubten Netzwerkverkehr zu begrenzen.

Die Container Definitions enthalten notwendigerweise die Informationen, um den Container zu starten. Hierzu gehören das zu verwendende Docker Image, dass über die AWS Elastic Container Registry referenziert wird. Hinzu kommen die PortMappings, die aussagen, welche Ports auf der Netzwerkschnittstelle an den Container weitergeleitet werden. Abschließend erfolgt noch die Konfiguration der Umgebungsvariablen und des im Container beim Start auszuführenden Kommandos. Dieses Kommando führt dann zur Laufzeit ein Skript aus, dass die SSH-Keys aus dem S3 Bucket lädt und dann den SSH-Server startet. Um die Logs der laufenden Container nicht zu verlieren, wird außerdem über die Eigenschaft LogConfiguration der awslogs-Treiber für die gestarteten Container konfiguriert. Dadurch sind die Logs nachher auf der CloudWatch-Logs-Konsole einsehbar.