Serverless App mit AWS Services

In diesem Artikel werden wir das Backend für eine Cloud Anwendung MyProjectManager  entwickeln. Die Anwendung soll u.A. den Benutzern die folgenden Operationen ermöglichen:

  • Anlage, Bearbeitung und Löschen von Aufgaben
  • Erfassung der Arbeitszeit der einzelnen Aufgaben
  • Monitoring des Fortschritts von aktiven Projekten
 
Der Cloud Anbieter, für den wir uns entschieden haben ist AWS (Amazon Web Services). AWS bietet eine Vielfalt von Services (c.a. 175), die ein breites Spektrum an Technologien und Branchenthemen abdecken. Diese Services gewährleisten ein hohes Maß an Sicherheit, Zuverlässigkeit,  Effizienz und Wirtschaftlichkeit. Eine direkte Konsequenz  einer vernünftigen Integration dieser Dienste in einer Anwendung ist die Flexibilität der Architektur, was wiederum die Wartbarkeit und Wiederverwendbarkeit der einzelnen Komponenten erheblich erleichtert.
In den folgenden Abschnitten, werden wir schrittweise die Implementierung unserer Anwendung durchgehen und die relevanten eingesetzten Technologien vorstellen.

Table of Contents

Überblick

Das folgende Diagramm stellt die Infrastruktur der Anwendung dar.

Infrastruktur der MyProjectsManager Anwendung

Implementierung und Konzepte

DynamoDb

Bevor wir in das Technische einsteigen, müssen wir unsere Modelle gemäß der Logik und dem Ziel der Anwendung definieren. Unsere Anwendung dreht sich um drei Hauptentitäten, nämlich das Projekt (Project), die Aufgabe (Task) und der Benutzer (User).
Jeder Benutzer hat einen Namen. Das Attribut „accumulated_workload“ ist die Anzahl der gearbeiteten Minuten. Dieses Feld wird pro erledigte Aufgabe um die entsprechende Arbeitszeit erhöht. Ein Benutzer kann an mehreren Projekten gleichzeitig arbeiten.
Jedem Projekt können  mehrere (oder keine) Mitarbeiter zugewiesen werden. Die Projekte bestehen aus Tasks, deren Status den Fortschritt des Projekts bestimmt.
Das „Task“ Objekt beschreibt den Titel, den Status und die gesamte Arbeitszeit einer Aufgabe, sowie der/die Mitarbeiter/in, der daran arbeitet.
Das „id“ Feld ist notwendig, um die einzelnen Objekten, identifizieren zu können.

Project

  • id: string
  • title: string
  • tasks: Task[]
  • assignees: string[]
  • progress: float

Task

  • id: string
  • title: string
  • assignee: string
  • completed: boolean
  • accumulated_workload: int

User

  • id: string
  • name: string
  • projects: string[]
  • accumulated_workload: int

Ein solcher Ansatz für unseren Anwendungsfall lässt sich in einer NoSQL Datenbank sehr einfach realisieren. Im AWS Umfeld redet man dann von der DynamoDb. Einer der größten Vorteile dieses Services ist die schnelle Leistung und nahtloser Skalierbarkeit, die er uns bietet. Darüber hinaus ist der Entwickler vom hohen Aufwand der administrativen Belastungen komplett befreit. Dies bedeutet, dass wir uns um die Hardwarebereitstellung, Einrichtung und Konfiguration nicht mehr kümmern müssen. Zudem sind ein paar Features vorhanden, die die Funktionalitäten des Services erweitern (z.B. DynamoDb Streams). Tabellen in der DynamoDb können beliebige Datenmengen beinhalten mit einer Durchsatzkapazität die jederzeit ohne Ausfall oder Leistungseinbuße hoch- oder herunterskaliert werden kann.


Die Anlage von Tabellen in DynamoDb ist in der AWS Console in nur ein paar Klicks erledigt (eine Zeile mit der CLI):

  • Im DynamoDb Service, links auf „Tables“ klicken
  • Auf „Create Table klicken“
  • Den Namen der Tabelle und des Primärschlüssels angeben

Der Primärschlüssel identifiziert jedes Element in der Tabelle eindeutig. DynamoDB unterstützt zwei verschiedene Arten von Primärschlüsseln :

  • Partitionsschlüssel: DynamoDB verwendet den Wert des Partitionsschlüssels als Eingabe für eine interne Hash-Funktion. Die Ausgabe der Hash-Funktion bestimmt die Partition (physischer interner Speicher von DynamoDB), in der das Element gespeichert wird.
  • Partitionsschlüssel und Sortierschlüssel: In einer Tabelle, die über einen Partitionsschlüssel und einen Sortierschlüssel verfügt, werden alle Elemente mit dem gleichen Partitionsschlüssel in der selben Partition gespeichert, werden aber nach dem Sortierschlüssel (eindeutig) sortiert.

Wir haben nun die Tabellen für die Projekte und die Benutzer angelegt. Allerdings haben wir bisher weder Projekte noch Benutzer. Wir können die natürlich manuell in der AWS Console anlegen. Das bringt uns aber nicht wirklich voran. Wir brauchen eine Schnittstelle, mit der wir u.a. gegen unsere Datenbank verschiedene CRUD Operationen ausführen können. AWS bietet SDKs für mehrere Programmiersprachen, mit denen wir das tun können. Wir brauchen allerdings eine Schnittstelle, die mehr als nur DynamoDb unterstützt, weil wir bestimmt noch andere Services brauchen werden. Eine solche Lösung heißt API Gateway“

API Gateway

API Gateway ist ein AWS Service zum Erstellen, Veröffentlichen, Warten, Überwachen und Sichern von REST-, HTTP-APIs und WebSocket-APIs in jeder Größenordnung. Die API kann auf externe (nicht von AWS) sowie AWS Web Services zugreifen (z.B. Lambda, Step Function, S3, usw…). Das folgende Diagramm ist ein Beispiel einer Architektur, die API Gateway mit anderen Services einsetzt.

Beispiel der Integration von API Gateway und andere AWS Services in einer Anwendung

API Gateway ist die primäre Schnittstelle mit der Clients, Daten vom Backend abrufen und Businesslogik ausführen. Der Service handhabt sämtliche Aufgaben im Zusammenhang mit der Annahme und Verarbeitung extrem vieler gleichzeitigen API-Aufrufe. Zu diesen Aufgaben gehören die Verwaltung des Datenverkehrs, Autorisierung und Zugriffskontrolle auf die entsprechenden Ressourcen. Hier äußern sich die Vorteile der Granularität einer Anwendung, die AWS Services umsetzt; ein Bauteil von mehreren wechselwirkenden Komponenten, dass das ganze System einrichten und steuern kann.

Für unseren Anwendungsfall brauchen wir eine REST API, die uns ermöglicht:

  • Projekte anzulegen, bearbeiten und löschen
  • Aufgaben für Projekte anzulegen, bearbeiten und löschen
  • Information über die Benutzer zu kriegen (textliche Beschreibung, Avatar, usw…)
  • Arbeitszeit zu erfassen

Die REST API ist eine Sammlung von Ressourcen und HTTP Methoden, die mit Backend-HTTP-Endpunkten, Lambda-Funktionen oder anderen AWS-Services integriert sind. Ein möglicher Lösungsvorschlag solcher API ist im nächsten Bild veranschaulicht.

Das Erstellen einer API Gateway ist auch recht unkompliziert.

  1. Im API Gateway Service oben links auf „APIs“ und dann auch „Create API“ klicken.
  2. REST-API wählen mit einem Klick auf „Build“.
  3. Einen Namen und eine Beschreibung der API eingeben.
  4. Regional als Typ des Endpunktes setzen

Lambda Funktionen

AWS Lambda ist ein Datenverarbeitungsservice, mit dem wir Code ausführen können, ohne Server bereitstellen oder verwalten zu müssen. Die gesamte Administration der Datenverarbeitungsressourcen, einschließlich der Server- und Betriebssystemwartung, Kapazitätsbereitstellung, automatische Skalierung sowie der Code-Überwachung und -Protokollierung ist dem Dienst überlassen. Amazon Lambda unterstützt mehrere Programmiersprachen, z.B. Python, Java, Node.js, usw…

Code in Lambda wird ausgeführt als Reaktion auf ein Ereignis. Die Ursprung des Ereignisses nennt man Auslöser. Dies kann z.B.. ein HTTP Aufruf eines Endpunktes der API Gateway sein, eine Änderung von Daten in einer S3 Bucket oder DynamoDb Tabelle sowie periodische Ereignisse (scheduled events). Das Objekt, das das Ereignis repräsentiert wird an die Funktion übermittelt, die ein Ergebnis zurückgibt oder Side-effects verursacht (oder beides). In diesem Kontext handelt es um eine „serverlosen“ Anwendung, in der kein Code läuft, wenn keine Anfrage verarbeitet werden muss.

Wir schauen nun wie wir diesen Dienst in unserer Anwendung verwenden können. In unserer API haben wir einen Endpunkt definiert, den wir aufrufen, um Projekte in der Datenbank anzulegen. Dafür verwenden wir die HTTP POST Methode. Der Payload soll mit dem von uns definierten Modell des Projekts übereinstimmen. Die HTTP Antwort soll das in der Projekttabelle angelegte Objekt zurückgeben. Hierfür werden wir den Code in Python schreiben.

 

import json
import boto3
import os
import uuid

def lambda_handler(event, context):
    
    project = event['project']
    projects_table_name = os.environ.get('PROJECTS_TABLE_NAME')
    
    if not dynamodb:
        dynamodb = boto3.resource('dynamodb')

    table = dynamodb.Table(projects_table_name)
    
    project['id'] = str(uuid.uuid4())
    response = table.put_item(
        Item=project,
        ReturnValues=ALL_NEW
        )
    
    newProject = response.get('Attributes')
    return {
        'statusCode': 200,
        'headers': {
            "Access-Control-Allow-Headers" : "*",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "OPTIONS,POST,GET"
        }
        'body': json.dumps(newProject)
    }

Wir können unsere implementierte Lambda Funktion sogar in der AWS Console testen. Wir stellen einen Payload bereit, führen die Funktion aus und schauen, ob in der Projekttabelle ein neues Projekt erscheint.

Der Rückgabewert von Lambda Funktionen ist meistens ein JSON Objekt, Text (z.B. base64, plain). Es ist natürlich auch möglich binäre Dateien zurückzugeben. Diese Dateien werden allerdings von einem Speicherdienst (s3) mittels einer SDK geholt und dann weitergeleitet (oder andersrum). Dies ist unvermeidlich, wenn eine spezifische Analyse oder Verarbeitung der Datei notwendig ist. In diesem Fall muss darauf geachtet werden, dass die Daten nicht verloren gehen, die richtigen HTTP Headers an den Clients gesendet werden und dass die Lambda Funktion über die richtigen Berechtigungen verfügt. Dahingegen würde der Aufwand für ein normales Down- oder Upload zu viel sein.

Dies betrifft unsere Anwendung, wenn wir z.B. die Avatars der Benutzer in einer S3 Bucket speichern wollen. Hier wäre es viel vernünftiger, die Lösung die uns AWS bietet einzusetzen – die direkte Integration von S3 in API Gateway.

S3 (Simple Storage Service)

Amazon S3 besitzt eine einfache Web-Service-Schnittstelle zum Speichern und Abrufen einer beliebigen Datenmenge zu jeder Zeit und von jedem Ort im Internet aus. S3 definiert hauptsächlich drei Schlüsselwörter:

Bucket: Ein Behälter für Objekte, die in Amazon S3 gespeichert werden. Buckets strukturieren den Amazon S3-Namespace auf der höchsten Ebene und werden auch für die Zugriffskontrolle eingesetzt. Überdies dienen sie im Rahmen der Erstellung von Nutzungsberichten als Auswertungseinheit.

Objekt: Objekte sind die Grundeinheiten, die in Amazon S3 gespeichert werden. Objekte bestehen aus Objekt- und Metadaten. Die Datenteile sind für Amazon S3 unverständlich. Metadaten bestehen aus mehreren Name/Wert-Paaren, die das Objekt beschreiben. Ein Objekt wird innerhalb eines Buckets eindeutig durch einen Schlüssel (Name) und eine Versions-ID identifiziert.

Schlüssel: Ein Schlüssel ist der eindeutige Bezeichner für ein Objekt in einem Bucket. Jedes Objekt in einem Bucket besitzt genau einen Schlüssel. Jedes Objekt wird durch die Kombination aus Bucket, Schlüssel und Versions-ID eindeutig identifiziert. Amazon S3 fungiert also als grundlegende Datenzuordnung zwischen „Bucket + Schlüssel + Version“ und dem Objekt selbst.

Zunächst legen wir einen neuen Bucket „my-project-manager-bucket“ an. In dem Bucket ein neues Objekt kreiert werden mit dem Schlüssel „userId“, was die ID des Benutzers entspricht, zudem alle Objekte unter diesem Verzeichnis gehören. Obwohl dieses Objekt an sich keine binäre Datei ist, bildet es eine logische Hierarchie und wird dann wie ein Ordner zu betrachten. Wenn der Benutzer einen Avatar hochlädt, wird das Bild mit dem Schlüssel „my-project-manager-bucket/{userId}/avatar“ angelegt, was bei jeder Abfrage des API Gateway Endpunktes heruntergeladen wird.

Die folgende Blöcke stellen die notwendigen Schritte der Integration von S3 in der API Gateway dar. Zuerst definieren wir einer „Ausführungsrolle„, die API Gateway im Runtime übernimmt, um auf unser Bucket zugreifen zu können. Zudem muss für die Rolle die Vertrauensstellung für API Gateway eingerichtet werden, damit API Gateway die Rolle übernehmen kann.

Definition der Ausführungsrolle
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::my-project-manager-bucket"
        }
    ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
Definition der Methode die S3 integriert

IAM Policies

Wir haben für alle bisher verwendeten Services bestimmte Policies und Rollen definieren müssen. Wir haben dennoch noch nicht gesehen, was diese Policies sind, wozu wir sie brauchen und wie die überhaupt funktionieren.

AWS Identity and Access Management (IAM) ist ein Webservice, mit dem wir den Zugriff auf AWS Ressourcen steuern.

Wir können mit IAM verschiedenen Benutzern verschiedene Berechtigungen für verschiedene Ressourcen erteilen. Das Beispiel haben wir bei der Implementierung von der Lambda Funktion „MyProjectManager-AddProject“ gesehen, in dem wir dieser Identität eine Rolle zugewiesen haben, die sie erlaubt, ausschließlich auf der „ProjectsTable“ Projekte anzulegen, zu listen, zu löschen und zu lesen. Das gleiche Prinzip gilt für andere AWS Ressourcen. Man unterscheidet hauptsächlich zwischen Typen von Berechtigungen und Richtlinien in AWS

Identitätsbasierte Richtlinien
steuern, welche Aktionen die Identität für welche Ressourcen und unter welchen Bedingungen ausführen kann

Ressourcenbasierte Richtlinien
steuern, welche Aktionen ein bestimmter Prinzipal mit dieser Ressource durchführen kann und unter welchen Bedingungen dies möglich ist.

Ein Prinzipal ist eine Person oder Anwendung, die eine Anforderung für eine Aktion oder Operation für eine AWS-Ressource stellen kann.

Step Functions

Häufig kommt es vor, dass in einer Anwendung als Reaktion auf ein Ereignis, ein bestimmter Prozess ausgeführt werden muss. Zum Beispiel eine Bestellung eines Produktes an der mehrere Parteien beteiligt sind. Um das Beispiel zu konkretisieren, nehmen wir an, dass die Reihenfolge wichtig ist. Falls eine Partei in der Mitte der Kette nicht benachrichtigt werden kann, muss ein Fallback Szenario stattfinden… (das kann natürlich noch komplizierter sein) Für einen solchen Fall muss eine saubere, wartbare und relativ unkompliezierte Lösung erdacht werden. 

Amazon Step Functions bietet ein Framework, das die Komplexität solcher Prozesse erheblich reduziert, in dem wir den Fokus auf dessen einzelnen Elementen setzen können und damit solche Szenarien überwinden können. Es ist ein serverloser Funktionsorchestrator, der die Sequenzierung von AWS Lambda-Funktionen und mehreren AWS-Services zu geschäftskritischen Anwendungen vereinfacht. Mit seinen integrierten Bedienelementen verwaltet Step Functions die Reihenfolge, die Fehlerbehandlung, die Wiederholungslogik und den Status und nimmt dem Team eine erhebliche operative Belastung ab.

Der Dienst kann für unsere Anwendung trotz Mangel an Komplexität eine wichtige Aufgabe erledigen- die Zeiterfassung. 
Den Prozess können wir wie folgend beschreiben.

 

  1. Die „State Machine“ wird von einem   regulären Ereignis getriggert (am ersten Tag jedes Monats)
  2. Die erste Lambda Funktion holt alle aktive Projekte von der „ProjectsTable“. Hier können wir den dynamischen Parallelismus, den die Step Functions unterstützen verwenden, um den Prozess zu beschleunigen, in dem für jedes Projekt ein neuer Knoten instantieert wird.
  3. Die zweite Lambda Funktion, die von jedem dieser Knoten ausgeführt wird, wird in der „UsersTable“ für jeden validen Benutzer das Attribut „accumulated_workload“ aktualisieren“. 
  4. Eine dritte Lambda Funktion erstellt ein Bericht (Abrechnung) der neuen Änderungen und leitet den an einen anderen Service weiter (SNS, SES, usw..) oder speichern den in S3.
  5. Die Aufgabe der vierten Lambda Funktion ist Clean-up.
{
  "Comment": "Billing",
  "StartAt": "GetProjects",
  "States": {
    "GetProjects": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:eu-central-1:<account_id>:function:MyProjectsManager-GetProjects",
      "Next": "ProcessProjects"
    },
    "ProcessProjects": {
      "Type": "Map",
      "MaxConcurrency": 5,
      "Iterator": {
        "StartAt": "UpdateWorkload",
        "States": {
          "UpdateWorkload": {
          "Type": "Task",
          "Resource": "arn:aws:lambda:eu-central-1:<account_id>:function:MyProjectsManager-UpdateWorkload",
          "End": true
          }
        }
      },
      "Next": "EmailBills"
    },
    "EmailBills": {
      "Type": "Pass",
      "Result": "Bills have been generated and emailed to the employees",
      "Next": "CleanUp"
    },
    "CleanUp": {
      "Type": "Pass",
      "Result": "Clean up complete",
      "End": true
    }
  }
}

Das Triggern der regulären Ereignissen kann u.a. durch die Definition einer Regel im Cron Syntax ( 0 0 1 * * ) erreicht werden. Regeln werden in EventBusse definiert (CloudWatch, EventBridge, usw…)

Cognito

Ein kritischer Punkt bei der Entwicklung von Web Anwendungen ist die Authentifizierung der ankommenden Anfragen, da wir meistens Zugriff auf sensiblen Daten auf eine bestimmte Gruppe beschränken wollen. Amazon Cognito nimmt uns dabei viel Aufwand ab mit seinen Authentifizierungs- und Autorisierungsmechanismen und seiner Benutzerverwaltung. Die Benutzer können sich direkt mit einem Benutzernamen und einem Passwort oder über einen Drittanbieter wie beispielsweise Facebook, Amazon, Google oder Apple anmelden.

Die zwei Hauptkomponenten von Amazon Cognito sind Benutzerpools und Identitäten-Pools. Benutzerpools sind Benutzerverzeichnisse, die Registrierungs- und Anmeldungsoptionen für die App-Nutzer bereitstellen. Identitäten-Pools ermöglichen es, den Benutzern Zugriff auf andere AWS-Services zu gewähren. Das folgende Bild dient der Veranschaulichung des Authentifizierungsprozesses.

  1. Der Benutzer meldet sich über einen Benutzerpool an und erhält nach erfolgreicher Authentifizierung Benutzerpool-Token.

  2. Die  Anwendung tausch die Benutzerpool-Tokens über einen Identitäten-Pool gegen AWS-Anmeldeinformationen aus.

  3. Nun kann der Benutzer diese AWS-Anmeldeinformationen nutzen, um auf andere AWS-Services wie Amazon S3 oder DynamoDB zuzugreifen.

Cognito bietet auch die Möglichkeit die Sign-up und Sign-in Prozesse durch Events (Post-Signup-Confirmation, Pre-Sign-In, etc…) zu verfeinern und darauf zu reagieren (Integration von AWS Services in diesen Prozessen, wie S3, Lambda oder DynamoDb, usw…). 

Nachdem der Userpool angelegt und eingerichtet ist, reicht es in der API Gateway einen neuen Authorizer anzulegen. Für jeden Endpunkt, den wir absichern möchten müssen wir in der Method Request den neu angelegten Authorizer setzen.

Sie finden hier eine Präsentation, die die oben vorgestellten Services von AWS und deren Eigenschaften zusammenfasst. 

AWS CloudFormation

AWS bietet uns eine alternative Vorgehensweise, die die Entwicklung und Einrichtung der AWS Services, die wir brauchen erleichtert. In einer YAML Datei, können wir unseren ganzen Stack beschreiben. Sobald die Vorlage validiert und deployed ist, kümmert sich CloudFormation um die Bereitstellung und Konfiguration der beschriebenen Ressourcen und deren Abhängigkeiten. Die Dokumentation beschreibt hier mehrere Szenarien, wo CloudFormation hilfreich sein kann. 

Folgend zeigen wir eine vereinfachte Version unserer oben beschriebenen API in der Form einer CloudFormation Vorlage.

API Gateway

Zunächst definieren wir eine REST API

 ApiGatewayRestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Description: API Gateway for the ProjectsManager Application
      EndpointConfiguration:
        Types:
          - REGIONAL
      Name: my-projects-manager-api-2

Die API Gateway braucht noch eine Stage und eine Deployment Ressource. Die definieren wir wie folgend

  ApiGatewayStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      DeploymentId: !Ref ApiGatewayDeployment
      Description: Lambda API Stage dev
      RestApiId: !Ref ApiGatewayRestApi
      StageName: 'dev'
  ApiGatewayDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: ApiGatewayMethod
    Properties:
      Description: Lambda API Deployment
      RestApiId: !Ref ApiGatewayRestApi

Wir legen nun zwei Ressourcen an und die entsprechenden Methoden. Wir möchten das Beispiel einer Lambda Integration (links) und einer S3 Integration (rechts) zeigen.

  
  ApiGatewayResourceProjects:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
      PathPart: 'projects'
      RestApiId: !Ref ApiGatewayRestApi
 ApiGatewayMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      ApiKeyRequired: false
      AuthorizationType: NONE
      HttpMethod: POST
      Integration:
        ConnectionType: INTERNET
        Credentials: !GetAtt ApiGatewayIamRole.Arn
        IntegrationHttpMethod: POST
        PassthroughBehavior: WHEN_NO_MATCH
        TimeoutInMillis: 29000
        Type: AWS_PROXY
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunctionAddProject.Arn}/invocations'
      OperationName: 'lambda'
      ResourceId: !Ref ApiGatewayResourceProjects
      RestApiId: !Ref ApiGatewayRestApi
  ApiGatewayResourceAttachments:
    Type: AWS::ApiGateway::Resource
    Properties:
      ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
      PathPart: 'attachments'
      RestApiId: !Ref ApiGatewayRestApi
  ApiGatewayMethodAttachmentsGET:
    Type: AWS::ApiGateway::Method
    Properties:
      ApiKeyRequired: false
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        ConnectionType: INTERNET
        Credentials: !GetAtt ApiGatewayIamRole.Arn
        IntegrationHttpMethod: GET
        PassthroughBehavior: WHEN_NO_MATCH
        TimeoutInMillis: 29000
        Type: AWS
        Uri: !Sub 
          - 'arn:aws:apigateway:${AWS::Region}:s3:path/${BucketName}/attachments'
          - { BucketName: !Ref S3BucketMyProjectsManager }
      OperationName: 'GetAttachments'
      ResourceId: !Ref ApiGatewayResourceAttachments
      RestApiId: !Ref ApiGatewayRestApi

Wenn wir eine POST Methode an unsere API schicken, wird die Lambda Funktion „LambdaFunctionAddProject“ ausgeführt werden. Diese Funktion soll das im Payload der Anfrage mitgesendete Projekt in einer DynamoDb Tabelle speichern. Dazu brauchen wir eine Lambda Funktion Ressource und eine DynamoDb Tabelle. 

DynamoDb
  ProjectsTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: ProjectsTable
      KeySchema:
        - AttributeName: "Id"
          KeyType: HASH
      AttributeDefinitions:
      - AttributeName: "Id"
        AttributeType: "S"
Lambda
 LambdaFunctionAddProject:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import boto3
          import os

          def handler(event, context):
            project = event.get('Project')
            dynamodb = boto3.resource('dynamodb')
            table_name = os.environ['PROJECTS_TABLE']
            table = dynamodb.Table(table_name)
            
            response = table.put_item(
              Item=project,
              ReturnValues=ALL_NEW
            )

            return {
              'statusCode': 200,
              'headers': {
                "Access-Control-Allow-Headers" : "*",
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Methods": "OPTIONS,POST,GET"
              }
              'body': json.dumps(response)
            }
      Description: AWS Lambda function
      FunctionName: 'add-project'
      Handler: index.handler
      MemorySize: 256
      Role: !GetAtt LambdaIamRoleDynamoDbAccess.Arn
      Runtime: python3.7
      Timeout: 60

Das Deployment der Lambda Funktion kann entweder Inline  (wie im Beispiel) beschrieben werden  oder als Deployment Package in einer S3 Bucket gespeichert werden, dessen Speicherort in der Vorlage angegeben werden soll. Dieser Artikel zeigt beide Alternativen.

S3 Bucket
S3BucketMyProjectsManager:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: "my-projects-manager"
IAM

Damit unser Stack funktional sein kann, müssen wir den verschiedenen Ressourcen die richtigen Berechtigungen geben. Dies betrifft:

  • Zugriff der Lambda auf die DynamoDb Tabelle
  • Ausführung der Lambda Funktionen von der API Gateway
  ApiGatewayIamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: ''
            Effect: 'Allow'
            Principal:
              Service:
                - 'apigateway.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      Policies:
        - PolicyName: LambdaAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action: 'lambda:*'
                Resource: !GetAtt LambdaFunctionAddProject.Arn
  LambdaIamRoleDynamoDbAccess:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Description: "Gives Read/Write Access DynamoDb to Lambda Functions executing this role"
      Path: '/'
      RoleName: 'lambda-dynamodb-read-write'
      Policies:
        - PolicyName: dynamodb-access
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action: '*'
                Resource: !GetAtt ProjectsTable.Arn

Um die Vorlage zu validieren können wir die AWS CLI benutzen:

aws cloudformation validate-template --template-body file://serverless.yml

Wenn keine Fehler gemeldet wurden, können wir das Deployment mit diesem Befehl durchführen

$ aws cloudformation deploy \
--stack-name my-projects-manager-api \
--template-file serverless.yaml \
--capabilities CAPABILITY_IAM

Schreibe einen Kommentar