In dem vorherigen Artikel haben wir einen Server-Proxy zwischen Amazon Web Services (AWS) S3 und einem Web-Frontend verwendet, um Dateien hochzuladen. Diesmal werden wir es serverless machen, ohne dass die Upload-Anfragen über einen Server weitergeleitet werden müssen. Dazu verwenden wir AWS Lambda und AWS S3 presigned URLs.
Presigned URLs gewähren für eine begrenzte Zeit direkten öffentlichen Zugriff auf private S3-Objekte, der durch die IAM-Berechtigungen (Identity and Access Management) des Benutzers, der die URL generiert, gesichert ist. Wir werden eine AWS Lambda-Funktion verwenden, um S3-vorgezeichnete URLs zu generieren, die von einem AWS API Gateway bereitgestellt werden.
Das folgende Diagramm veranschaulicht die Abläufe beim Hochladen einer Datei über eine S3 presigned URL:
- Abrufen der S3 presigned URL.
- Hochladen der Datei zu S3 über eine presigned URL.
Erstellen von S3 presigned URLs
Zur Implementierung der Lambda-Funktion, die die presigned URLs generiert, haben wir die neueste Version der Node.js-Laufzeitumgebung (v20) und AWS JS SDK v3 gewählt. Hier ist der Code der Funktion:
import * as AWS from "@aws-sdk/client-s3";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Configuration = {
region: process.env.AWS_REGION_NAME,
};
const client = new S3Client(s3Configuration);
export const handler = async (event, context) => {
const key = event.queryStringParameters.key
const command = new PutObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: key });
const uploadURL = await getSignedUrl(client, command, { expiresIn: process.env.URL_EXPIRATION_SECONDS });
return {
"statusCode": 200,
"isBase64Encoded": false,
"headers": {
"Access-Control-Allow-Origin": "*"
},
"body": JSON.stringify({
"uploadURL": uploadURL,
"key": key
})
};
}
Die Code-Implementierung erzeugt ein S3 Client-Objekt, indem sie die Region des S3-Buckets angibt, in die wir Objekte hochladen möchten. Wenn keine Region angegeben ist, versucht das SDK, sie aus der Ausführungsumgebung der Lambda-Funktion (AWS_REGION) oder aus gemeinsam genutzten Konfigurationsdateien zu ermitteln.
Da die Aufrufe unserer Lambda-Funktion durch das AWS API Gateway ausgelöst werden, können wir davon ausgehen, dass die Struktur des Event-JSON-Objekts die Eigenschaft queryStringParameters enthält, in der wir eine beliebige Anzahl von Eigenschaften übergeben können. In unserem Fall erwarten wir einen „Schlüssel“-Abfrageparameter, der dem S3-Schlüssel des Objekts entspricht, das über die generierte presigned URL im Bucket gespeichert werden soll.
Als Nächstes konfiguriert die handler-Funktion einen PutObjectCommand für den Bucket und den betreffenden Objektschlüssel, der zusammen mit dem S3Client-Objekt und einem Konfigurationsobjekt, das die URL-Ablaufzeit in Sekunden angiebt, als Eingabeparameter an die Funktion getSignedUrl des SDK übergeben wird. Aus Sicherheitsgründen sollte die Ablaufzeit so kurz wie möglich sein, um das Risiko eines unbefugten Zugriffs auf unsere Ressourcen zu verringern.
Schließlich liefert die handler-Funktion ein JSON-Objekt, das Folgendes enthält:
- statusCode: Der HTTP-Erfolgsstatuscode.
- isBase64Encoded: Eine Kennung, die angibt, ob der Antworttext mit Base64 codiert ist. In diesem Fall soll er nicht Base64-codiert sein, da wir keine Binärdaten übertragen.
- headers: Zusätzliche Header, um die gemeinsame Nutzung von Ressourcen über verschiedene Quellen hinweg (CORS) für Webanwendungen zu ermöglichen, die unseren API-Gateway-Endpunkt aufrufen, der wiederum diese Lambda-Funktion aufruft.
Die handler-Funktion muss immer ein Promise zurückgeben oder die Callback-Funktion verwenden, da wir sonst einen HTTP-Fehler 502 vom API-Gateway mit der Meldung „Interner Serverfehler“ erhalten. In den API-Gateway-Protokollen sehen wir auch den Fehler: „Ausführung aufgrund eines Konfigurationsfehlers fehlgeschlagen: Fehlerhafte Lambda-Proxy-Antwort“.
Hinweis: Die Lambda-Funktionshandler-Datei sollte die Erweiterung „.mjs“ haben, damit sie als ES-Modul gekennzeichnet ist. Alternativ muss eine package.json-Datei mit dem Typ „module“ konfiguriert werden. Auf diese Weise können wir die Import-Anweisung verwenden, um andere ES-Module zu importieren, z. B. die AWS S3 SDK-Module. Wenn die Datei eine „.js“-Erweiterung hat, aber kein Typ „module“ in einer package.json konfiguriert ist, erhalten wir beim Importieren von Modulen mit der ES-Import-Anweisung die Fehlermeldung „SyntaxError: Import-Anweisung kann nicht außerhalb eines Moduls verwendet werden“.
Konfiguration von Umgebungsvariablen
Für einen saubereren Code verwendet der Lambda-Funktionscode Umgebungsvariablen für den Namen der Region, den Namen des Buckets und die URL-Ablaufzeit in Sekunden. Um Umgebungsvariablen für eine Lambda-Funktion hinzuzufügen oder zu bearbeiten, können wir dies auf der Registerkarte „Configuration“ tun, gefolgt von „Environment“ im Seitenmenü, wie im folgenden Screenshot dargestellt:
Hinzugefügte/bearbeitete Umgebungsvariablen stehen dem Node-Prozess der Funktion sofort zur Verfügung und können mit dem Node-Objekt process.env gelesen werden, wie z. B. process.env.BUCKET_NAME.
Hinweis: Es wird empfohlen, allgemeine Anwendungseigenschaften zentral über Dienste wie AWS System Manager (SSM) Parameter Store zu verwalten, insbesondere bei sensiblen Daten wie z. B. Anmeldedaten.
Berechtigungskonfiguration
Die generierten presigned URLs übernehmen die Berechtigungen der IAM-Rolle der Lambda-Funktion. Wenn unsere Lambda-Funktion nicht über S3 mit den erforderlichen Schreibberechtigungen verfügt, die mit ihrer IAM-Rolle verknüpft sind, können die ausgestellten presigned URLs keine Dateien im Namen von Kunden in den betreffenden S3-Bucket hochladen, und wir erhalten eine Fehlermeldung, die in etwa wie folgt lautet:
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>ASDGssffscSDQ4POL</RequestId>
<HostId>OLsT4/9tYWsdssyo0dxtFa7sdsdsKhdPqsdsd4L+9CmiKP2tFyGsdsL8Pr0E0rkDgzHsddsVjdsdsdwc=</HostId>
</Error>
Folgen wir dem Prinzip des geringsten Privilegs (PoLP) der Cybersicherheit und fügen eine Inline-Richtlinie hinzu, die nur den S3-Vorgang PutObject zulässt. Gehen Sie zur Registerkarte „Configuration“ der Lambda-Seite, wählen Sie „Permissions“ im Seitenmenü aus und klicken Sie dann unter „Execution role“ auf den Link des Rollennamens, um die Seite der IAM-Rolle zu öffnen:
Klicken Sie auf der Seite „IAM-Rolle“ der Lambda-Funktion auf der Registerkarte „Permissions“ auf die Schaltfläche „Add permissions“ und wählen Sie „Create inline policy“ aus:
Wählen Sie auf der Seite „create policy“ JSON aus und fügen Sie die folgende Richtlinie mit der ARN des S3-Buckets hinzu:
Fügen Sie als Nächstes den Richtliniennamen hinzu und klicken Sie auf die Schaltfläche „Create policy“:
API-Gateway-Konfiguration
Um unsere Lambda-Funktion für Front-End-Apps verfügbar zu machen, verwenden wir AWS API Gateway. Eine Möglichkeit, dies zu tun, ist über die Schaltfläche „Add trigger“ (Auslöser hinzufügen) im Lambda-Diagramm unserer Funktion:
Wählen Sie auf der Seite „Add trigger“ (siehe Abbildung unten) API Gateway als Quelle aus und klicken Sie dann auf „Create a new API“ (Neue API erstellen) mit HTTP API als API-Typ. HTTP API ist ein schlankerer API-Typ mit geringer Latenz und für unsere Zwecke mehr als ausreichend. Der Kürze halber und nur zu Demonstrationszwecken wählen wir „Open“ als Sicherheit, d. h. jeder kann auf den API-Endpunkt zugreifen. Beachten Sie, dass es insbesondere in einer Produktionsumgebung dringend empfohlen wird, die API mit z. B. AWS Cognito User Pools oder Lambda Authorizers zu sichern.
Sobald der API-Gateway trigger hinzugefügt wurde, ist er im Lambda-Diagramm sichtbar und der entsprechende Endpunkt ist auf der Registerkarte „Configuration“ unter „Triggers“ wie folgt sichtbar:
Im nächsten Abschnitt werden wir diese API-Endpunkt-URL verwenden, um S3 presigned URLs zu erhalten.
Hochladen einer Datei mit einer S3 presigned URL
Um die S3 presigned URL zum Hochladen einer Datei zu erhalten, rufen wir den AWS API Gateway-Endpunkt auf, der einen Aufruf der zuvor erstellten Lambda-Funktion auslöst, und verwenden hierfür Postman.
Im nächsten Screenshot sehen wir eine GET-Anfrage und eine erfolgreiche Antwort auf den zuvor erwähnten Endpunkt. Der JSON-Antworttext enthält die Eigenschaft uploadURL mit der presigned URL als Wert.
Schließlich erstellen wir eine PUT-Anfrage in Postman und verwenden die presigned URL, die aus der Antwort der vorherigen Anfrage kopiert werden kann. Auf der Registerkarte „Body“ der Anfrage sollten wir „binary“ auswählen und eine Datei von unserer lokalen Festplatte auswählen, die wir über die presigned URL in S3 hochladen möchten, wie in der folgenden Abbildung dargestellt. Nachdem wir auf die Schaltfläche „Send“ geklickt haben, sollte eine erfolgreiche Antwort mit einem leeren Textkörper und dem HTTP-Status 200 angezeigt werden:
Hinweis: Bei Dateien, die größer als 100 MB sind, wird empfohlen, mehrteilige Uploads durchzuführen.
Fazit
Mit AWS S3 presigned URLs können Dateien auf skalierbarere, effizientere und einfachere Weise direkt hochgeladen werden, statt mit einem Server-Upload-Proxy. Dies kann zu niedrigeren Betriebskosten beitragen, indem die Bandbreite und die Verarbeitungslast auf den Servern reduziert werden, und somit die Skalierbarkeit erhöht wird, da diese nicht mit dem Datenverkehr beim Datei-Upload umgehen müssen.
Gleichzeitig verringert es die Latenzzeit für Datei-Upload-Anfragen und ermöglicht das Hochladen großer Dateien, indem es die Nutzlastbegrenzungen des Middleware-Servers umgeht, wie z. B. die 10-MB-Nutzlastbegrenzung für Anfragen von AWS API Gateway.
Aber wie bei allem anderen sollten auch bei der Wahl von vorgefertigten AWS S3-URLs einige Nachteile berücksichtigt werden:
- Wenn ein böswilliger Benutzer in den Besitz der presigned URL gelangt, kann er Dateien in den Bucket hochladen, bis die URL abläuft. Das Risiko lässt sich durch die Festlegung einer kurzen URL-Ablaufzeit verringern. Die Risiken können auch weiter gemindert werden, z. B. durch die Implementierung von Mechanismen zur Begrenzung der Anzahl der Verwendungen einer URL.
- Durch die Vermeidung der Verwendung von Servern kann man nicht einfach benutzerdefinierte Middleware oder Verarbeitungsschritte zum Datei-Upload-Ablauf hinzufügen, wie z. B. Formatvalidierung oder Virenscans
- Es kann auch zu einer eingeschränkten Beobachtbarkeit und Kontrolle über den Upload-Prozess kommen. Überwachung und Protokollierung sind auf das beschränkt, was S3 und CloudWatch bieten, was möglicherweise nicht so umfassend ist wie das, was Sie mit einer serverseitigen Verarbeitung erreichen könnten. Es kann schwieriger sein, ein detailliertes Prüfprotokoll der Datei-Uploads zu führen, da der Upload-Verkehr nicht über unsere Server läuft.
Lesen Sie hier den Artikel über Multipart-Upload mit Amazon S3.