Zum Hauptinhalt springen

Object Storage Client Tools

Neben Integrationen in Ihr Entwicklungs-Framework gibt es dutzende Tools, um auf Buckets zuzugreifen. Wir stellen hier Konfigurationen für die gängigsten Tools vor. Für weitere Details konsultieren Sie am besten direkt die Dokumentation zur jeweiligen Software.

Backend v1 vs. v2

Je nach Backend Version unterscheiden sich gewisse Konfigurations-Parameter.

API Region

Bei v1-Buckets stimmt der physikalische Standort mit dem region-Parameter überein. Sofern der Bucket also in nine-cz42 angelegt wurde, muss der region-Parameter auch auf nine-cz42 gesetzt werden. Dies ändert sich bei v2-Buckets, bei welchen die region immer auf us-east-1 gesetzt werden muss. Der Grund dafür ist, dass us-east-1 die Standard-Region bei AWS ist und viele Tools dies automatisch so setzen. Dies verbessert die Kompatibilität, da manche Tools auch gar nicht die Möglichkeit bieten, die region ausserhalb von AWS zu konfigurieren.

Physikalischer Standortv1 Regionv2 Region
nine-cz42nine-cz42us-east-1
nine-es34nine-es34us-east-1

API Hostname

Der Hostname, um auf die S3 API zuzugreifen, unterscheidet sich je nach Backend-Version.

Physikalischer Standortv1 Hostnamev2 Hostname
nine-cz42cz42.objectstorage.nineapis.chcz42.objects.nineapis.ch
nine-es34es34.objectstorage.nineapis.ches34.objects.nineapis.ch

Hostformat und Pfadformat für Bucket-Zugriff

Für v2-Buckets ist der Zugriff auf den Bucket nun auch direkt über den Hostnamen möglich und nicht nur über den Pfad:

Pfadformat: https://cz42.objects.nineapis.ch/bucket-name

Hostformat: https://bucket-name.cz42.objects.nineapis.ch

s3cmd

s3cmd ist ein beliebtes Command Line Tool zum Verwalten der Daten in Object Storage Buckets. Das Tool ist für AWS S3 ausgelegt, funktioniert aber problemlos mit anderen kompatiblen Systemen, wie unserem Object Storage. Um es zu konfigurieren, benötigen Sie Angaben zu Ihrem Bucket und zum entsprechenden User. Stellen Sie sicher, dass der User Zugriff auf den Bucket hat.

Dies ist eine Beispiel Konfiguration, welche in einer Datei ~/.s3cfg liegen soll. Passen sie die Werte entsprechend den Informationen für Ihren User und Bucket an.

[default]
access_key = 6aaf50b17357446bb1a25a6c93361569
secret_key = fcf1c9c6bc5c4384a4e0dbff99d3cc52
host_base = cz42.objects.nineapis.ch
host_bucket = cz42.objects.nineapis.ch
use_https = True

Danach können Daten hochgeladen werden.

s3cmd put image01.jpg s3://my-bucket

rclone

rclone wird häufig für das Backup von Daten auf einen S3 kompatiblen Speicher verwendet. Es gibt aber zahlreiche weitere Backends für verschiedene andere Speicher.

Dies ist eine Beispiel Konfiguration, welche in einer Datei ~/.config/rclone/rclone.conf liegen soll. Passen sie die Werte entsprechend den Informationen für Ihren User und Bucket an.

[nine-cz42]
type = s3
provider = Other
access_key_id = 6aaf50b17357446bb1a25a6c93361569
secret_access_key = fcf1c9c6bc5c4384a4e0dbff99d3cc52
endpoint = https://cz42.objects.nineapis.ch

Danach können Dateien von der lokalen Disk auf den Object Storage hochgeladen werden.

rclone copy image01.png nine-cz42:my-bucket

Achtung Wegen eines Bugs muss bei neueren Versionen von rclone noch das Flag --s3-no-check-bucket angegeben werden.

Oder Sie können ein lokales Verzeichnis mit dem Object Storage synchronisieren:

rclone sync backup-dir nine-cz42:mybucket

restic

restic ist eine ausserordentliche Backup Lösung, die einfach zu bedienen ist und über sehr gute Deduplizierung verfügt.

Achtung! Bei älteren Versionen von restic kann ein Problem mit den Länderregionen-Settings auftreten. Bitte verwenden Sie Version ab v0.12.0 und aufwärts.

Ähnlich wie bei anderen Tools müssen als erstes die s3 Variablen definiert werden. Dies kann mittels Umgebungsvariablen gesetzt werde oder über Command Line Parameter. Im folgenden Beispiel verwenden wir Umgebungsvariablen, da diese in der Regel praktischer sind:

Wir definieren die folgenden Variablen in der Konfigurationsdatei: ~/my_backup/restic.conf

export AWS_ACCESS_KEY_ID="6aaf50b17357446bb1a25a6c93361569"
export AWS_SECRET_ACCESS_KEY="fcf1c9c6bc5c4384a4e0dbff99d3cc52"
export RESTIC_REPOSITORY="s3:https://cz42.objects.nineapis.ch/bucket-etj4mwciyzuv"
export RESTIC_PASSWORD="SUPER-SECURE-PASSWORD"

Anders als bei anderen Tools arbeitet restic mit Repositories, daher müssen wir anfänglich ein solches erstellen. Dafür müssen wir die im vorherigen Schritt definierten Umgebungsvariablen laden:

$ source ~/my_backup/restic.conf

Nun kann das Repository initialisiert werden:

$ restic init

Jetzt ist alles bereit um Backups anzulegen:

$ restic backup /home/www-data/this_should_be_backuped

Um zu prüfen, was für Backups bereits bestehen:

$ restic snapshots
repository ff06b869 opened successfully, password is correct
ID Time Host Tags Paths
-------------------------------------------------------------------------------------------------------------------
bb689eb9 2021-07-16 11:45:27 mysuperserver /home/www-data/this_should_be_backuped.bin
-------------------------------------------------------------------------------------------------------------------
1 snapshots

Ein Backup Vorgang erstellt einen Snapshot. Ältere Snapshots können wie folgt gelöscht werden:

$ restic forget bb689eb9 --prune

Um ältere Snapshots nicht manuel löschen zu müssen, kann dies mit Retention Policies automatisiert werden:

$ restic forget --keep-daily 30 --keep-weekly 4 --keep-monthly 12 --keep-yearly 10 --prune

Das hier dargestellte Beispiel ist eine sehr simple Anwendung von restic für das Sichern eines Ordners. Das Tool ist zu weitaus mehr fähig und bietet eine Vielzahl von zusätzlichen Optionen. Sie finden unter folgendem Link detaillierte Dokumentationen der Funktionen:

https://restic.readthedocs.io/en/stable/040_backup.html

Objektspeicher im Code verwenden

Die Verwendung des Objektspeichers im Code ist etwas komplizierter als die Nutzung des Dateispeichers. Glücklicherweise gibt es eine Vielzahl an Programmbibliotheken, die diese Aufgabe vereinfachen. Hier schauen wir uns ein Beispiel in Javascript unter Verwendung der minio-Bibliothek an.

Dazu installieren wir zunächst die benötigten Abhängigkeiten mit npm. Für dieses Beispiel beziehen wir die Client-Informationen mittels dotenv. Dies lässt sich für Ihren Anwendungsfall anpassen. npm install minio dotenv

Um den untenstehende Code im Typescript-Format (dies vereinfacht die Erkennung, welche Objekttypen genutzt werden) direkt zu verwenden, muss ausserdem npm install --save-dev @types/minio ausgeführt werden, um die nötigen Abhängigkeiten zu installieren.

Zunächst muss ein Client mit den korrekten Informationen aus der Umgebung erstellt werden:

import * as Minio from "minio"

//Dadurch erhalten wir die Umgebungs-Variablen aus process.env
require("dotenv").config()

// Erstellen des Client mit den korrekten Daten und Nutzung von Default-Values, insofern die env Variablen nicht existieren
// Bitte beachten Sie, dass sowohl ein Key als auch ein Secret bereitgestellt werden muss, um den Objektspeicher zu nutzen!
const client = new Minio.Client({
endPoint: process.env.BUCKET_ENDPOINT || "cz42.objects.nineapis.ch",
region: process.env.BUCKET_REGION || "us-east-1",
useSSL: true,
accessKey: process.env.BUCKET_KEY || "",
secretKey: process.env.BUCKET_SECRET || "",
})

Die dazugehörige .env Datei würde so aussehen:

BUCKET_ENDPOINT=cz42.objects.nineapis.ch  # or es34.objects.nineapis.ch
BUCKET_REGION=us-east-1
BUCKET_KEY=my-bucket-key-from-cockpit-or-api
BUCKET_SECRET=my-bucket-secret-from-cockpit-or-api

Da wir nun eine funktionierende Verbindung zu unserem Objektspeicher geschaffen haben, müssen noch einige Funktionen erstellt werden, um dort Daten auszulesen oder zu speichern. Die minio-Bibliothek bietet zwei Möglichkeiten, um Daten auszulesen. Wir können sie entweder als Datei auf die lokale Festplatte schreiben, oder die Inhalte der Datei in den Arbeitsspeicher streamen. Wenn Sie den Objektspeicher nutzen, ist es gut denkbar, dass Ihnen keine Dateisysteme zum direkten Speichern zur Verfügung stehen. Wir sehen uns hier daher den Streaming-Ansatz an. Das Streamen von Daten ist komplexer als das Speichern einer Datei auf einer Festplatte, da hier der Datenstrom-Status beachtet werden muss und die Datei stückweise übertragen wird. Diese „Chunks" müssen weiterverarbeitet werden. Dies kann mit den folgenden Funktionen bewerkstelligt werden:

// Daten werden asynchron aus Ihrem Objektspeicher ausgelesen. Es wird ein Promise ausgegeben, der
// entweder einen Puffer an Daten wiedergibt oder Null anzeigt, wenn der Key nicht existiert. Dies führt zu Fehlern

async function read(bucket: string, key: string): Promise<Buffer | null> {
return new Promise(async (resolve, reject) => {
// Wir fasssen Code mit Try/Catch ein, da ein Fehler auftreten kann,​ bevor der Stream wiedergegeben wird

try {
// Der Stream, den wir auslesen werden. Bitte beachten Sie, dass Sie das Stream-Handling
// auch der Funktion getObject überlassen können. Wenn Sie die Daten einfach wiedergeben möchten,
// ist es leichter, auf den Stream zu warten und ihn dann weiter zu bearbeiten
const stream = await client.getObject(bucket, key)

// Wir nutzen dieses Array, um die Chunks aus dem Stream zu erhalten
const chunks: any[] = []

stream.on("data", (chunk) => {
// Wenn der Stream angibt, dass er Daten hat, werden diese dem Array hinzugefügt
chunks.push(chunk)
})

stream.on("end", () => {
// Bei "end" haben wir die gesamte Datei im Array als Chunks gespeichert.
// Wir wandeln sie in einen Puffer um und schicken diesen zur Verarbeitung weiter.
// Wenn Ihr Stream eine JSON-Datei ist, könnte diese Verarbeitung so aussehen:
// JSON.parse(returnedBuffer.toString())
const fileContent = Buffer.concat(chunks)
resolve(fileContent)
})

stream.on("error", (err) => {
// Wenn der Stream einen Fehler wiedergibt, lehnen wir den Promise ab,
// statt ihn mit "resolve" zu lösen, und geben die erhaltene Fehlermeldung aus
reject(err)
})
} catch (error: any) {
// Sollte ein Fehler beim Streamen der Datei auftreten, wird dieser hier behandelt
if (error.code === "NoSuchKey") {
// Für diesen Fall liefern wir für den "Promise" ein resolve zurück, da es sich streng
// genommen nicht immer um einen Fehler handelt, wenn kein Key vorhanden ist.
// Zu prüfen, ob ein Key existiert, bevor ein Schreibvorgang durchgeführt wird,
// ist ein oft genutzter Anwendungsfall. Um dies zu prüfen, muss die Rückgabe
// lediglich auf "null" geprüft werden.
resolve(null)
} else {
// Jeder Fehler ausser NoSuchKey wird als kritisch betrachtet und führt dazu,
// dass der Promise abgelehnt wird
reject(error)
}
}
})
}

// Daten werden asynchron in den Puffer geschrieben. Wenn dies funktioniert, wird "promise true" zurückgeliefert.
// Andernfalls wird ein Fehler ausgegeben.
async function write(bucket: string, key: string, data: any): Promise<boolean> {
return new Promise(async (resolve, reject) => {
// Daten werden in den Objektspeicher gelegt - die Daten müssen ein lesbarer Stream sein
// Weitere Informationen hierzu finden Sie in der minio Dokumentation:
// https://min.io/docs/minio/linux/developers/javascript/API.html#putobject-bucketname-objectname-stream-size-metadata-callback
// Das einfachste Code-Beispiel wäre JSON.stringify(my-data-string-var)
client.putObject(bucket, key, data, (err, data) => {
if (err) reject(err)
else resolve(true)
})
})
}