Skalierbarer Cloud-Speicher für Ihr Web-Projekt

cloud storage

Mit dem Simple Storage Service (S3) bietet Amazon einen skalierbaren Objektspeicher mit einfacher Webservice-Schnittstelle an. Dabei bezahlt man lediglich den Speicher, den man auch tatsächlich nutzt. Die ersten 5 GB erhält man zudem kostenlos.

Die übertragenen Daten speichert Amazon S3 als Objekte in sogenannten Buckets. Eine Begrenzung der Objektanzahl innerhalb eines Buckets gibt es nicht.

 

Weshalb sollte ich meine Binärdaten in der Cloud speichern?

Es gibt einige Vorteile, die der Amazon S3 Speicher gegenüber einem gewöhnlichen Storage hat:

 

  • Zugriffskontrolle konfigurierbar
    wer darf Objekte erstellen, löschen und abrufen
  • AWS Region wählbar
    die Latenz zum Abruf der Daten kann optimiert werden, indem man die Daten in genau die Regions verteilt, von wo sie auch abgerufen werden
  • Ereignisbenachrichtigung nutzen
    Lassen Sie sich benachrichtigen, wenn Objekte in Amazon S3 hochgeladen wurden und nutzen Sie diese für eigene Workflows
  • Metadaten
    Sie haben Kontrolle über die Metadaten eines jeden Objekts. Nutzen Sie diese beispielsweise zur Steuerung des Caching-Verhaltens oder zur Anreicherung mit Ihren systemrelevanten Informationen (Aufnahmeort eines Bildes, Lizenzdaten, Author, …).
  • Verwaltung des Lebenszyklus
    Konfigurieren Sie selbst die Lebensdauer und Archivierung Ihrer Objekte. Gerade bei CMS-gesteuerten Prozessen spielt die Depublikation unter Umständen eine wichtige Rolle.
  • Versionierung
    Einzelne Versionen eines Objekts können abgerufen und wiederhergestellt werden.

 

AWS S3 REST API

Um die Vorteile von Amazon S3 nutzen zu können, benötigt man einen einfachen Weg, um Bilder oder Dokumente der eigenen Webanwendung im Cloudstorage zu verwalten. Nutzt man für seine Inhalte ein Content Management System, dann liegt es nahe, binäre Daten aus dem CMS direkt in S3 zu speichern.

Zugriff auf seinen Amazon S3 Speicher erhält man über sogenannte Access-Keys. Diese bestehen aus einer AccessKeyID und einem Secret access key. Beides kann man sich nach der AWS Anmeldung generieren lassen.

Es bietet sich an, die sehr gut dokumentierte REST API zu nutzen. Die Kommunikation erfolgt über HTTP und ist abgesichert durch den Athentication Header.

Ein Beispiel – als Ergebnis des folgenden GET-Requests, erhält man eine Liste aller Buckets, für die man Zugriffsrechte besitzt:

Request

GET / HTTP/1.1
Host: s3.amazonaws.com
Date: Wed, 20 Jan 2015 12:00:00 GMT
Authorization: my_authorization_string

Response

<?xml version="1.0" encoding="UTF-8"?>
<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2015-01-20"
  <Owner>
    <ID>myID</ID>
    <DisplayName>myDisplayName</DisplayName>
  </Owner>
  <Buckets>
    <Bucket>
      <Name>sample</Name>
      <CreationDate>2015-01-01T12:00:00.000Z</CreationDate>
    </Bucket>
  </Buckets>
</ListAllMyBucketsResult>

 

Der Authentication Header

Damit kein anderer User oder keine andere Anwendungen die S3-Objekte verändern kann, ist die Kommunikation über den sogenannten Authentication Header abgesichert. Amazon nutzt hierfür den Standard HTTP Authorization Header. Es ist zwar etwas verwirrend, dass der Authorisierungs-Header die Authentisierungsdaten überträgt, wird aber ausdrücklich so in der Dokumentation erwähnt.

Der Authorization-String besteht aus 4 Teilen:

  1. String „AWS „
  2. AWSAccessKeyID
  3. String „:“
  4. Signature

Die Signature wiederum ist ein Base64 codierter HMAC-SHA1 String aus der AccessKeyID und einem weiteren SignIn-String. Amazon selbst beschreibt den Aufbau über folgenden Pseudocode:

Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;

Signature = Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) );

StringToSign = HTTP-Verb + "\n" +
     Content-MD5 + "\n" +
     Content-Type + "\n" +
     Date + "\n" +
     CanonicalizedAmzHeaders +
     CanonicalizedResource;

CanonicalizedResource = [ "/" + Bucket ] +
     <HTTP-Request-URI, from the protocol name up to the query string> +
     [ subresource, if present. For example "?acl", "?location", "?logging", or "?torrent"];

CanonicalizedAmzHeaders = <described below>

Der Prozess zur Generierung des CanonicalizedAmzHeaders ist hier dokumentiert: Constructing the CanonicalizedAmzHeaders

 

Das Perl-Modul Net::Amazon::S3

Eine einfache Anbindung an Amazon S3 bietet das Perl-Modul Net::Amazon::S3 und besteht aus folgenden Submodulen:

Net::Amazon::S3
Net::Amazon::S3::Bucket
Net::Amazon::S3::Client
Net::Amazon::S3::Client::Bucket
Net::Amazon::S3::Client::Object
Net::Amazon::S3::HTTPRequest
Net::Amazon::S3::Request
Net::Amazon::S3::Request::CompleteMultipartUpload
Net::Amazon::S3::Request::CreateBucket
Net::Amazon::S3::Request::DeleteBucket
Net::Amazon::S3::Request::DeleteMultiObject
Net::Amazon::S3::Request::DeleteObject
Net::Amazon::S3::Request::GetBucketAccessControl
Net::Amazon::S3::Request::GetBucketLocationConstraint
Net::Amazon::S3::Request::GetObject
Net::Amazon::S3::Request::GetObjectAccessControl
Net::Amazon::S3::Request::InitiateMultipartUpload
Net::Amazon::S3::Request::ListAllMyBuckets
Net::Amazon::S3::Request::ListBucket
Net::Amazon::S3::Request::ListParts
Net::Amazon::S3::Request::PutObject
Net::Amazon::S3::Request::PutPart
Net::Amazon::S3::Request::SetBucketAccessControl
Net::Amazon::S3::Request::SetObjectAccessControl

Das Zusammenspiel der Module generiert jeweils den HTTP-Request zur Ausführung der gewünschten Aktion. Der Vorteil bei der Nutzung von Net::Amazon::S3 besteht darin, dass man sich nicht mehr um die Zusammensetzung des Headers, Timeouts oder Proxy-Einstellungen kümmern muss. Es stellt entsprechende Konfigurationsparameter und Funktionen bereit.

 

Eine simple Beispielklasse

Die folgende Klasse enthält Methoden, um Buckets und Keys (S3 Objekte) zu erstellen und zu löschen:

#!/bin/false
# vim: set autoindent shiftwidth=4 tabstop=8:
package Project::Services::AmazonS3;

use strict;

require Project::Util::Logger;
require Project::Util::Configuration;

use Data::Dump qw(dump);

require Net::Amazon::S3;

sub new {
    my ($class, %args) = @_;

    my $logger = Project::Util::Logger->new;
    my $conf = Project::Util::Configuration->new;

    my $aws_access_key_id = $conf->getAmazonS3ServiceAccessKeyId
        || die("'AMAZON_S3_SERVICE_ACCESS_KEY_ID' missing in project.conf!");
    my $aws_secret_access_key = $conf->getAmazonS3ServiceSecretKey
        || die("'AMAZON_S3_SERVICE_SECRET_KEY' missing in project.conf!");

    my $aws_service_proxy = $conf->getAmazonS3ServiceProxy || '';

    my $s3 = Net::Amazon::S3->new({
                aws_access_key_id        => $aws_access_key_id,
                aws_secret_access_key    => $aws_secret_access_key,
                timeout                  => 10,
                retry                    => 0,
                proxy                    => $aws_service_proxy
    });

    $logger->debug("Created AmazonS3 instance with 'aws_access_key_id' => ".
        "'$aws_access_key_id' and 'aws_secret_access_key' => ".
        "'***'.");

    unless(defined $s3) {
        $logger->error("Could not define Amazon S3 object with ".
            "'aws_access_key_id' => '$aws_access_key_id' and ".
            "'aws_secret_access_key' => '***'!");
        return undef;
    }

    my $self = {
        '__logger'    => $logger,
        '__conf'      => $conf,
        '__s3'        => $s3,
    };
    bless $self, $class;
    
    return $self;
}

### buckets
sub createBucket {
    my ($self, $bucket_name, $acl, $location) = @_;
    
    my $logger = $self->{__logger};
    my $s3 = $self->{__s3};
    
    $logger->debug("createBucket '$bucket_name'.");
    
    my $bucket = $s3->add_bucket({
                            bucket               => $bucket_name,
                            acl_short            => $acl,
                            location_constraint  => $location,
    });
    unless (defined $bucket) {
        $logger->error("Couldn't create bucket '$bucket_name'! ".
            "'".$s3->err."' : '".$s3->errstr."'.");
        return undef;
    }
    
    return
        unless($self->__checkBucket($bucket));

    return $bucket;
}

sub existsBucket {
    my ($self, $bucket_name) = @_;
    
    my $logger = $self->{__logger};
    my $s3 = $self->{__s3};
    
    $logger->debug("Exists bucket with name '$bucket_name'.");
    
    my $buckets = $self->getBuckets;
    foreach my $bucket (@$buckets) {
        if ($bucket_name eq $bucket->{bucket}) {
            $logger->debug("Bucket found.");
            return $bucket;
        }
    }
    
    return;
}

sub getBucket {
    my ($self, $bucket_name) = @_;
    
    my $logger = $self->{__logger};
    my $s3 = $self->{__s3};
    
    $logger->debug("Get bucket with name '$bucket_name'.");
    
    my $bucket = $s3->bucket($bucket_name);
    unless (defined $bucket) {
        $logger->error("Couldn't get bucket '$bucket_name'! ".
            "'".$s3->err."' : '".$s3->errstr."'.");
        return undef;
    }
    
    return $bucket;
}

sub getBuckets {
    my ($self) = @_;
        
    my $logger = $self->{__logger};
    my $s3 = $self->{__s3};

    $logger->debug("Get all buckets.");
    
    my $buckets = $s3->buckets->{buckets};
    unless(defined $buckets) {
        $logger->error("Couldn't get all buckets! ".
            "'".$s3->err."' : '".$s3->errstr."'.");
        return undef;
    }
    unless (ref($buckets) eq 'ARRAY') {
        $logger->error("Return value is no ARRAY!");
        return undef;
    }
    
    return $buckets;
}

sub deleteBucket {
    my ($self, $bucket) = @_;
    
    my $logger = $self->{__logger};
    
    $logger->debug("Delete bucket.");
    
    if (ref($bucket) eq 'Net::Amazon::S3::Bucket') {
        unless ($bucket->delete_bucket) {
            $logger->error("Couldn't delete bucket! ".
                "'".$bucket->err."' : '".$bucket->errstr."'.");
            return undef;
        }        
    } elsif (ref($bucket) eq 'HASH') {
        my $s3 = $self->{__s3};
        unless ($s3->delete_bucket($bucket)) {
            $logger->error("Couldn't delete bucket! ".
                "'".$bucket->err."' : '".$bucket->errstr."'.");
            return undef;
        }
    } else {
        $logger->error("Bucket has to be either an object of ".
            "'Net::Amazon::S3::Bucket' or a HASH-ref!");
        return undef;
    }
    
    return 1;
}


### keys
sub createKey {
    my ($self, $bucket, $keyname, $value, $metadata) = @_;
    
    my $logger = $self->{__logger};
    
    $keyname = $self->__cleanupKeyname($keyname);
    
    $logger->debug("createKey '$keyname' with metadata '".
                        dump($metadata)."'.");
    
    return
        unless($self->__checkBucket($bucket));
    
    unless (ref($metadata) eq 'HASH') {
        $logger->error("Option 'metadata' has to be a HASH-ref!");
        return undef;
    }
    
    unless($bucket->add_key($keyname, $value, $metadata)) {
        $logger->error("Couldn't create key '$keyname'! ".
            "'".$bucket->err."' : '".$bucket->errstr."'.");
        return undef;
    }
    
    $logger->debug("\t Uploaded image '$keyname' successful.");

    return 1;
}

sub getKey {
    my ($self, $bucket, $keyname) = @_;
    
    my $logger = $self->{__logger};
    
    $keyname = $self->__cleanupKeyname($keyname);
    
    $logger->debug("getKey '$keyname'.");
    
    return
        unless($self->__checkBucket($bucket));


    my $key = $bucket->get_key_filename($keyname);
    unless(defined $key) {
        $logger->error("Couldn't get key '$keyname'! ".
            "'".$bucket->err."' : '".$bucket->errstr."'.");
        return undef;
    }
    unless(ref($key) eq 'HASH') {
        $logger->error("Expected 'HASH'! Got '".ref($key)."'.");
        return undef;
    }
    
    return $key;
}

sub getKeys {
    my ($self, $bucket) = @_;
    
    my $logger = $self->{__logger};
    
    $logger->debug("getKeys.");
    
    return
        unless($self->__checkBucket($bucket));

    my $bucket_list = $bucket->list;
    unless(defined $bucket_list) {
        $logger->error("Couldn't get keys! ".
            "'".$bucket->err."' : '".$bucket->errstr."'.");
        return undef;
    }

    my $keys = $bucket_list->{keys};
    unless (ref($keys) eq 'ARRAY') {
        $logger->error("Option 'keys' has to be a ARRAY! Got '".ref($keys)."'.");
        return undef;
    }
    
    return $keys;
}

sub deleteKey {
    my ($self, $bucket, $keyname) = @_;
    
    my $logger = $self->{__logger};
    
    $keyname = $self->__cleanupKeyname($keyname);
    
    $logger->debug("deleteKey '$keyname'.");
    
    return
        unless($self->__checkBucket($bucket));
        
    unless($bucket->delete_key($keyname)) {
        $logger->error("Couldn't delete key '$keyname'! ".
            "'".$bucket->err."' : '".$bucket->errstr."'.");
        return undef;
    }
    
    return 1;
}


sub __checkBucket {
    my ($self, $bucket) = @_;
    
    my $logger = $self->{__logger};
    unless (ref($bucket) eq 'Net::Amazon::S3::Bucket') {
        $logger->error("Bucket is no object of 'Net::Amazon::S3::Bucket'!".
            " Object is '".ref($bucket)."'.");
        return;
    }
    
    return $bucket;
}

sub __cleanupKeyname {
    my ($self, $keyname) = @_;
    
    $keyname =~ s|^/||; 
    
    return $keyname;
}

1;

 

Media Assets ab in die Cloud!

Die Klasse Project::Services::AmazonS3 lässt sich hervorragend dazu nutzen, um Media Assets von imperia CMS im Amazon S3 Storage zu verwalten.

Für diesen Anwendungsfall ist die MessageCheck-Schnittstelle ideal geeignet. Ein MessageCheck-PlugIn reagiert auf Notifications, die vom imperia CMS bei Publikation oder Depublikation von imperia-Dokumenten generiert werden. Realisiert habe ich dieses Szenario mit dem PlugIn MAMCloudStorage. Es überprüft, ob in einem Dokument Medien-Objekte des imperia Media Asset Managements (MAM) verknüpft wurden.

Wird ein verknüpftes Bild erkannt, lädt das PlugIn es in den Amazon S3 Cloud Storage. Dabei wird zuvor geprüft, ob es schon vorhanden ist und ob sich eventuell verändert hat.

Umgekehrt verhält es sich bei Depublikation – wird ein Dokument gelöscht, prüft das PlugIn zunächst, ob das zuvor verwendete Bild in anderen Dokumenten referenziert ist. Ist dies nicht der Fall, wird das Bild umgehend entfernt.

 

imperiaCMS-amazonS3

 

Aber bitte keine Amazon-URL im Quelltext

Will man vermeiden, dass im HTML-Quelltext Amazon-URLs enthalten sind, sollte man diese mittels Reverse-Proxy umschreiben.
Der Apache-Webserver stellt hierfür das Modul mod_proxy bereit.

Angenommen die CMS-Bilder liegen im Bucket http://angileri.s3.amazonaws.com/cms/images/, man will sie aber über http://angileri.de/cms/images/ ausspielen, kann das über folgende Apache-Konfiguration realisiert werden:

ProxyPass  /cms/images/   http://angileri.s3.amazonaws.com/cms/images/

Zuvor bitte sicherstellten, dass folgende Module geladen werden:

LoadModule proxy_module /usr/lib/apache2/modules/mod_proxy.so
LoadModule proxy_http_module /usr/lib/apache2/modules/mod_proxy_http.so

Nachdem der Webserver neu gestartet wurde, werden S3-Bilder über die eigene Domain ausgeliefert – die Amazon-URL bleibt dem Client verborgen. Zudem hat man nun die Möglichkeit, weitere Caching-Optimierungen vorzunehmen.

 

tl;dr

  • Amazon-S3 bietet einige Vorteile gegenüber herkömmlichem Storage
  • Amazon bietet via REST-API die Möglichkeit, Aktionen mittels HTTP-Requests im S3 vorzunehmen
  • Net::Amazon::S3 ist eine simple Lösung, um Perl Applikationen wie imperia CMS mit S3 zu verknüpfen
  • Über Reverse-Proxy Einstellungen lässt sich die Amazon-AWS URL gegenüber dem Client verbergen
Bitte bewerten Sie meinen Beitrag:
grauenhafteinfach nur schlechtich habe es geschafft ohne einzuschlafengut ist gut genugsehr gut, bitte mehr davon (2 Bewertungen, Ø: 5,00 von 5)
Loading...