Dateien im Stapel mit curl und AWS CLI nach Amazon S3 exportieren
In vielen Entwicklungs-Workflows ist der direkte Export von Dateien in Cloud-Speicher wie Amazon S3
eine gängige Anforderung. Die Automatisierung dieses file export-Prozesses reduziert Fehler und
beschleunigt Ihren Workflow. In dieser Anleitung erklären wir, wie Sie Dateien im Stapel nach
Amazon S3 exportieren, indem Sie pre-signed URLs mit AWS CLI erzeugen und die Dateien
mit curl in einem Bash-Skript hochladen. Das ergibt einen Ansatz auf Basis von open source.
Voraussetzungen
Bevor Sie beginnen, benötigen Sie:
- AWS CLI Version 2, installiert und mit den passenden Zugangsdaten konfiguriert.
curl, auf Ihrem System installiert.- Node.js 24 oder neuer, das TypeScript-Dateien direkt ausführt. Der Signierer weiter unten benötigt es.
- Eine Bash-Shell-Umgebung.
- Einen vorhandenen S3-Bucket mit den nötigen Berechtigungen.
- Einen IAM-Benutzer oder eine IAM-Rolle, die S3-Aktionen ausführen kann.
AWS CLI v2 installieren (Linux):
# For x86_64 systems
curl -fsSL --retry 3 "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
# For arm64 systems
curl -fsSL --retry 3 "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o awscliv2.zip
unzip awscliv2.zip
sudo ./aws/install
rm -rf awscliv2.zip aws/ # Clean up: aws/ is a directory, so -f alone will not remove it
aws --version # Should output aws-cli/2.x.x ...
Konfigurieren Sie AWS CLI. Der Signierer im nächsten Abschnitt liest dieselbe Zugangsdaten-Datei,
dieser eine Schritt deckt also beide Werkzeuge ab:
aws configure
# Enter:
# - AWS Access Key ID [None]: your_access_key
# - AWS Secret Access Key [None]: your_secret_key
# - Default region name [None]: your-region (for example, us-east-1)
# - Default output format [None]: json (or leave blank)
Installieren Sie die beiden AWS-SDK-Pakete, die der Signierer importiert:
mkdir s3-batch-export && cd s3-batch-export
npm init -y
npm pkg set type=module
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Die Komponenten verstehen
Amazon S3 ist ein skalierbarer Objektspeicher. Eine pre-signed URL gewährt zeitlich begrenzt die
Berechtigung, eine ganz bestimmte Art von S3-Anfrage auszuführen, ohne Ihre AWS-Zugangsdaten
gegenüber der ausführenden Partei preiszugeben. Die Signatur umfasst die HTTP-Methode, Bucket und
Key, die Ablaufzeit sowie alle Header, die Sie signieren möchten. Eine für ein GET signierte URL
lässt sich daher nicht als PUT wiederverwenden.
Es handelt sich nicht um ein Einmal-Token. Dieselbe URL lässt sich beliebig oft einlösen, bis sie
abläuft, und jede Einlösung überschreibt das Objekt unter diesem Key. Betrachten Sie sie als
Berechtigung mit Verfallsdatum: Geben Sie sie an genau eine Partei weiter, halten Sie sie aus Logs
und Referrern heraus und halten Sie --expires-in kurz.
Dieses Detail entscheidet über die Werkzeugwahl. Der Befehl aws s3 presign ist ausschließlich für GET
dokumentiert: „So kann jede Person, die die vorsignierte URL erhält, das S3-Objekt mit einer
HTTP-GET-Anfrage abrufen.“ Er kennt kein Flag --method, seine Ausgabe kann also kein curl -T
autorisieren; S3 beantwortet einen solchen Versuch mit SignatureDoesNotMatch. Um ein PUT zu signieren,
brauchen Sie einen Signierer, mit dem Sie die Operation benennen können, und genau das leistet
@aws-sdk/s3-request-presigner.
AWS CLI behält hier seinen Platz für die Einrichtung und Überprüfung der Zugangsdaten, und curl
führt weiterhin die Übertragung durch.
Die erforderlichen IAM-Berechtigungen vergeben
Die Signatur selbst wird lokal berechnet, beim Erzeugen der URL wird also keine Berechtigung geprüft. Entscheidend sind die Berechtigungen des Signierers, denn S3 wertet beim Einlösen der vorsignierten URL die Richtlinie der signierenden Identität aus. Hängen Sie dem IAM-Benutzer oder der IAM-Rolle, deren Zugangsdaten Sie konfiguriert haben, eine Richtlinie wie diese an:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
s3:GetObject brauchen Sie, wenn Sie Downloads vorsignieren möchten; für das hier beschriebene
Upload-Szenario ist jedoch nur s3:PutObject erforderlich. Eine vorsignierte URL kann nie mehr
gewähren, als der Signierer ohnehin besitzt. Wenn Sie diese Richtlinie verschärfen, verschärfen
Sie damit jede URL, die Sie herausgeben.
Ersetzen Sie your-bucket-name durch Ihren tatsächlichen Bucket-Namen.
Vorsignierte PUT-URLs mit dem AWS SDK erzeugen
Speichern Sie dies als presign-put.ts. Das Skript gibt eine URL aus und beendet sich, wodurch es sich
später leicht aus einer Shell-Schleife heraus aufrufen lässt:
// presign-put.ts - print a pre-signed URL that authorizes PUT, and only PUT.
import { parseArgs } from 'node:util'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const { values } = parseArgs({
options: {
bucket: { type: 'string' },
key: { type: 'string' },
region: { type: 'string' },
'content-type': { type: 'string', default: 'application/octet-stream' },
'expires-in': { type: 'string', default: '3600' },
},
})
const { bucket, key, region } = values
if (!bucket || !key || !region) {
throw new Error('Usage: node presign-put.ts --bucket B --key K --region R [--content-type T]')
}
const expiresIn = Number(values['expires-in'])
// SigV4 caps a pre-signed URL at 7 days. Reject longer values here rather than
// relying on the SDK's generic expiry error; fractional values are rejected too.
if (!Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 604800) {
throw new Error(`--expires-in must be between 1 and 604800 seconds, got: ${values['expires-in']}`)
}
const client = new S3Client({
region,
// Default 'WHEN_SUPPORTED' hoists a CRC32 of an *empty* body into the query
// string. S3 enforces it against the bytes curl actually sends, so every
// upload would fail. Pre-signed PUTs must opt out.
requestChecksumCalculation: 'WHEN_REQUIRED',
})
const url = await getSignedUrl(
client,
new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: values['content-type'] }),
{
expiresIn,
// Pin Content-Type into the signature. S3 honors whatever Content-Type the
// uploader sends either way; signing it is what stops the uploader from
// sending a *different* one, since any mismatch now fails the signature.
signableHeaders: new Set(['content-type']),
},
)
console.log(url)
Führen Sie es aus:
node presign-put.ts \
--bucket your-bucket-name \
--key object-key \
--region your-region \
--content-type text/plain \
--expires-in 3600
Ersetzen Sie your-bucket-name, object-key (den gewünschten Namen der Datei in S3) und your-region
durch Ihre konkreten Werte. Die zurückgelieferte URL trägt die Signatur und ihre Ablaufzeit im
Query-String; sie enthält keinen geheimen Schlüssel, aber wer sie besitzt, kann dieses eine Objekt
wiederholt schreiben, bis sie abläuft. Halten Sie sie deshalb aus Logs heraus.
--expires-in ist eine Obergrenze, keine Garantie. Wenn Sie mit temporären Zugangsdaten signieren, etwa
aus einer IAM-Rolle, einer SSO-Sitzung oder sts:AssumeRole, funktioniert die URL ab dem Moment nicht mehr,
in dem diese Zugangsdaten ablaufen, und das ist oft deutlich vor der von Ihnen angeforderten
Ablaufzeit.
X-Amz-SignedHeaders lautet in der erzeugten URL content-type;host. Genau diese Liste muss curl
exakt reproduzieren. Wenn Sie einen abweichenden Content-Type senden oder den Header weglassen, wird
die Signatur ungültig. Die maximale Ablaufzeit bei SigV4 beträgt 7 Tage (604.800 Sekunden).
Dateien mit curl hochladen
Nutzen Sie bei curl das Flag -T, um die lokale Datei anzugeben, die hochgeladen werden
soll. Mit --retry wird der Upload widerstandsfähiger gegen vorübergehende Netzwerkprobleme. Der
Header Content-Type ist hier nicht optional: Er gehört zu den signierten Headern und muss daher mit
dem --content-type übereinstimmen, das Sie dem Signierer übergeben haben.
Die Subshell begrenzt die strikte Fehlerbehandlung auf dieses Beispiel, selbst wenn Sie es in eine
bestehende Bash-Sitzung einfügen.
(
set -euo pipefail
# Determine the MIME type dynamically (works on Linux and macOS)
CONTENT_TYPE=$(file -b --mime-type localfile.txt)
# Sign for that exact type, then send that exact type
URL=$(node presign-put.ts \
--bucket your-bucket-name \
--key localfile.txt \
--region your-region \
--content-type "$CONTENT_TYPE")
if status=$(curl -fsS --retry 3 --retry-delay 2 \
-T localfile.txt \
-H "Content-Type: $CONTENT_TYPE" \
-o /dev/null -w '%{http_code}' \
"$URL") && [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
echo 'Upload completed.'
else
echo 'Upload failed: expected an HTTP 2xx response.' >&2
exit 1
fi
)
Ersetzen Sie localfile.txt durch Ihren Dateipfad sowie Bucket und Region durch Ihre eigenen Werte. Mit
-f beendet sich curl bei einem HTTP-Fehler mit einem Exit-Code ungleich null, statt die
XML-Fehlerantwort von S3 auszugeben, als wäre alles gut gegangen, und -sS hält die
Fortschrittsanzeige still, zeigt echte Fehler aber weiterhin an. Die explizite 2xx-Prüfung weist
zudem Redirects zurück, die -f allein nicht als Fehler behandelt. Fügen Sie kein
-L hinzu: Einem Redirect zu folgen würde den Body erneut an eine URL senden, die die Signatur
nicht abdeckt.
Stapel-Dateiexporte automatisieren
Dutzende Dateien manuell hochzuladen ist mühsam. Das folgende Skript durchläuft ein Verzeichnis,
signiert für jede Datei ein PUT, lädt sie mit curl hoch und beendet sich anschließend mit
einem Exit-Code ungleich null, wenn auch nur eine einzelne Datei fehlgeschlagen ist, damit ein
Cronjob oder CI-Schritt das auch tatsächlich mitbekommt.
#!/usr/bin/env bash
set -euo pipefail # Exit on error, undefined variable, or pipe failure
BUCKET="your-bucket-name"
EXPIRE=3600 # URL validity in seconds (1 hour)
FILES_DIR="/path/to/your/files" # Directory containing files to upload
REGION="your-region" # Your S3 bucket region
PRESIGN="./presign-put.ts" # The signer from the previous section
# --- pre-flight checks ---
if [[ ! -d "$FILES_DIR" ]]; then
echo "Error: Directory '$FILES_DIR' does not exist." >&2
exit 1
fi
if [[ ! -f "$PRESIGN" ]]; then
echo "Error: Signer '$PRESIGN' not found. See the previous section." >&2
exit 1
fi
if ! command -v aws &>/dev/null; then
echo "Error: AWS CLI command not found. Please install AWS CLI v2." >&2
echo "See: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" >&2
exit 1
fi
# Verify the credentials the signer will pick up are present and valid.
if ! caller_arn=$(aws sts get-caller-identity --query Arn --output text 2>/dev/null); then
echo "Error: AWS credentials are not configured properly or are invalid." >&2
echo "Please run 'aws configure' or check your environment variables/IAM role." >&2
exit 1
fi
echo "AWS credentials verified for: $caller_arn"
echo "Starting batch export from '$FILES_DIR' to bucket '$BUCKET' in region '$REGION'..."
# --- processing loop ---
failed=0
uploaded=0
# dotglob: a hidden file is an ordinary object to S3, and silently leaving
# .env.example behind is data loss. nullglob: an empty directory must not run
# the loop once with a literal '*'.
shopt -s dotglob nullglob
for file in "$FILES_DIR"/*; do
filename=$(basename "$file")
# Test for a symlink before -f, which follows them: a link inside FILES_DIR
# would otherwise upload bytes from outside it, under a name from inside it.
if [[ -L "$file" ]]; then
echo " Skipping '$filename': symbolic link."
continue
fi
# Skip directories and anything else that is not a regular file.
[[ -f "$file" ]] || continue
echo "Processing '$filename'..."
# A single PUT tops out at 5 GiB; larger objects need multipart upload.
# stat -f%z works on macOS/BSD, stat -c%s works on Linux.
file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")
if ((file_size > 5368709120)); then
echo " Skipping '$filename': $((file_size / 1024 / 1024)) MiB exceeds the 5 GiB PUT limit." >&2
echo " Use 'aws s3 cp' instead, which switches to multipart automatically." >&2
failed=$((failed + 1))
continue
fi
content_type=$(file -b --mime-type "$file")
echo " Signing a PUT for '$filename' (Content-Type: $content_type)..."
if ! url=$(node "$PRESIGN" \
--bucket "$BUCKET" \
--key "$filename" \
--region "$REGION" \
--content-type "$content_type" \
--expires-in "$EXPIRE"); then
echo " Error: Failed to sign a URL for '$filename'." >&2
failed=$((failed + 1))
continue
fi
echo " Uploading '$filename'..."
# The Content-Type must match the signature. curl -f does not reject redirects;
# require a completed 2xx upload without replaying a signed body at another URL.
if status=$(curl -fsS --retry 3 --retry-delay 2 \
-T "$file" \
-H "Content-Type: $content_type" \
-o /dev/null -w '%{http_code}' \
"$url") && [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
echo " Successfully uploaded '$filename'."
uploaded=$((uploaded + 1))
else
echo " Error: Failed to upload '$filename'." >&2
failed=$((failed + 1))
fi
done
echo "Batch export completed: $uploaded uploaded, $failed failed."
# Surface partial failure to the caller: 'set -e' cannot do this for us, because
# every failure above was deliberately caught so the loop could continue.
if ((failed > 0)); then
exit 1
fi
Denken Sie daran, your-bucket-name, /path/to/your/files und your-region im Skript zu ersetzen. Machen Sie
das Skript ausführbar (chmod +x script_name.sh), bevor Sie es starten.
Drei Einschränkungen sollte man klar benennen. Das Skript verwendet den Dateinamen als Objekt-Key,
gleichnamige Dateien in verschiedenen Unterverzeichnissen würden also kollidieren. Deshalb liest es
nur die oberste Ebene von FILES_DIR. curl --retry sendet die gesamte Datei ab Byte null erneut,
denn ein vorsigniertes PUT kennt keine Wiederaufnahme: Ein 4 GiB großer Upload, der bei 90 %
abbricht, beginnt von vorn. Und ein Upload ist ein bedingungsloses Überschreiben, ein erneuter Lauf
des Skripts ersetzt also alles, was bereits unter diesen Keys liegt. Aktivieren Sie die
Bucket-Versionierung, wenn Sie die vorherigen Bytes zurückholen müssen.
Bewährte Sicherheitsverfahren anwenden
Beim Arbeiten mit Cloud-Ressourcen hat Sicherheit oberste Priorität:
- IAM-Rollen bevorzugen: Wenn Sie Skripte auf EC2-Instanzen oder anderen AWS-Diensten ausführen, nutzen Sie IAM-Rollen für temporäre Zugangsdaten statt langlebiger Zugriffsschlüssel.
- Least Privilege: Vergeben Sie nur die für diese Aufgabe nötige Berechtigung
s3:PutObject, begrenzt auf den konkreten Bucket. - Kurze URL-Lebensdauern: Halten Sie den Wert
--expires-infürpre-signed URLs so kurz, wie es für die Upload-Dauer praktikabel ist. - Verschlüsselung: Aktivieren Sie serverseitige Verschlüsselung (SSE-S3, SSE-KMS oder SSE-C) für Ihren S3-Bucket, um Daten im Ruhezustand zu schützen.
- Monitoring: Das Berechnen einer Signatur erzeugt kein CloudTrail-Ereignis, es wird also
nirgends festgehalten, dass eine URL ausgestellt wurde. Beobachten lässt sich die Einlösung:
Aktivieren Sie das S3-Serverzugriffs-Logging oder CloudTrail-Datenereignisse für den Bucket, die
die resultierenden Aufrufe von
PutObjectprotokollieren. Da eine URL während ihrer gesamten Lebensdauer gültig bleibt, fällt Ihnen genau in diesen Logs auch auf, wenn eine erneut eingelöst wird. - VPC-Endpunkte: Wenn Ihr Skript innerhalb einer VPC läuft, nutzen Sie S3-VPC-Endpunkte, damit der Datenverkehr im AWS-Netzwerk bleibt und das öffentliche Internet meidet.
Häufige Probleme beheben
- Der Signierer schlägt fehl, bevor überhaupt ein Upload startet: Die Signatur wird lokal
berechnet, das Auflösen der Zugangsdaten ist aber nicht immer lokal: Die Standard-Provider-Kette
ruft für eine SSO- oder Rollen-Sitzung womöglich STS auf oder auf EC2 den Instance Metadata
Service, und beides kann fehlschlagen oder in eine Zeitüberschreitung laufen. Fehlende
Zugangsdaten zeigen sich als
CredentialsProviderError. Prüfen Sie Ihre Einrichtung mitaws sts get-caller-identity. SignatureDoesNotMatchbeim Upload mitcurl: Die Anfrage weicht von dem ab, was signiert wurde. Übliche Ursachen sind einContent-Type, der nicht zu--content-typepasst, ein Redirect mit-Loder eine URL, die erneut gequotet oder von der Shell expandiert wurde. Setzen Sie die URL immer in doppelte Anführungszeichen: Sie enthält&.Access Deniedbeim Upload mitcurl: Prüfen Sie, ob die Richtlinie der signierenden Identitäts3:PutObjectaufarn:aws:s3:::your-bucket-name/*erlaubt, und kontrollieren Sie Bucket-Richtlinien, die Einstellungen für Block Public Access oder eine KMS-Schlüsselrichtlinie, die den Schreibvorgang verweigern könnte.XAmzContentChecksumMismatchoderBadDigest: Die URL wurde mit dem Standard-Prüfsummenmodus des SDK signiert, der eine CRC32 eines leeren Bodys festschreibt. Setzen SierequestChecksumCalculationwie oben gezeigt auf'WHEN_REQUIRED'.- Netzwerk-Zeitüberschreitungen (von
curlgemeldete Fehler): Erhöhen Sie die Zahl der Wiederholungen über--retryoder ergänzen Sie die Optionen--connect-timeout/--max-timefürcurl, wenn Sie es mit langsamen Netzwerken zu tun haben. Prüfen Sie Netzwerkverbindung und Firewalls. - Datei zu groß (Fehler
EntityTooLargeoder Überspringen im Skript): Bei Dateien über 5 GiB funktioniert der einzelne PUT-Vorgang nicht, den diese Methode mitcurlverwendet. Nutzen Sie beiAWS CLIden Befehlaws s3 cp, der Multipart-Uploads für große Dateien automatisch abwickelt. - Falsche Region (
AuthorizationHeaderMalformed): Der Wert--region, den Sie dem Signierer übergeben, steckt sowohl im Hostnamen als auch im Credential-Scope und muss daher zur tatsächlichen Region des Buckets passen.aws s3api get-bucket-location --bucket your-bucket-nameverrät Ihnen, welche das ist. - Abgelaufene URL (
AccessDeniedoderRequest has expired): Diepre-signed URList nur für die mit--expires-inangegebene Dauer gültig. Erzeugen Sie die URL neu, wenn der Upload länger dauert als erwartet oder erst nach Ablauf versucht wird. - Falscher Content-Type: Wenn sich Dateien nach dem Download nicht wie erwartet verhalten, prüfen
Sie den Header
Content-Type, der beim Upload mitcurlgesetzt wurde, noch einmal. Stellen Sie sicher, dassfile --mime-typeden korrekten Typ liefert.
Sie möchten etwas hochladen, das größer als 5 GiB ist? Überlassen Sie AWS CLI die Komplexität des
Multipart-Chunkings:
# AWS CLI handles multipart uploads automatically for large files
aws s3 cp /path/to/your/large_file.zip s3://your-bucket-name/
Fazit
Ein kleiner AWS-SDK-Signierer in Kombination mit curl für die Übertragung ergibt eine flexible
Pipeline auf Basis von open source für den Stapelexport von Dateien nach Amazon S3, ohne dass
Zugangsdaten im Übertragungsbefehl selbst stehen. Der Signierer ist genau der Teil, den aws s3 presign
nicht abdecken kann, da dessen URLs immer nur ein GET autorisieren. Alles Übrige bleibt bei
AWS CLI: Einrichtung und Überprüfung der Zugangsdaten sowie der Rückfallweg für große Dateien.
Für komplexere Workflows, bei denen Dateien vor oder nach dem file export nach S3 verarbeitet
werden, sollten Sie einen Managed Service in Betracht ziehen. Unser
Robot 🤖 /s3/store etwa setzt ein ähnliches
Muster um und erlaubt Ihnen, Ergebnisse aus anderen verarbeitenden
Steps innerhalb einer einzigen Assembly direkt nach S3 zu exportieren.
Viel Spaß beim Programmieren!
