Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions

Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions
Macchina da caffè Rubé Goldberg

L'architettura basata sugli eventi aumenta l'efficienza in termini di costi delle risorse utilizzate perché vengono utilizzate solo nel momento in cui sono necessarie. Esistono molte opzioni su come implementarlo e su come non creare entità cloud aggiuntive come applicazioni di lavoro. E oggi non parlerò di FaaS, ma di webhook. Mostrerò un esempio tutorial sulla gestione degli eventi utilizzando i webhook di archiviazione di oggetti.

Qualche parola sull'archiviazione di oggetti e sui webhook. Lo storage degli oggetti consente di archiviare qualsiasi dato nel cloud sotto forma di oggetti, accessibili tramite S3 o un'altra API (a seconda dell'implementazione) tramite HTTP/HTTPS. I webhook sono generalmente callback HTTP personalizzati. In genere vengono attivati ​​da un evento, ad esempio il codice inviato a un repository o un commento pubblicato su un blog. Quando si verifica un evento, il sito di origine invia una richiesta HTTP all'URL specificato per il webhook. Di conseguenza, puoi fare in modo che gli eventi su un sito attivino azioni su un altro (wiki). Nel caso in cui il sito di origine sia un oggetto di archiviazione, gli eventi agiscono come modifiche al suo contenuto.

Esempi di casi semplici in cui è possibile utilizzare tale automazione:

  1. Creazione di copie di tutti gli oggetti in un altro archivio cloud. Le copie devono essere create al volo ogni volta che i file vengono aggiunti o modificati.
  2. Creazione automatica di una serie di miniature di file grafici, aggiunta di filigrane alle fotografie e altre modifiche alle immagini.
  3. Notifica sull'arrivo di nuovi documenti (ad esempio, un servizio di contabilità distribuita carica i report nel cloud e il monitoraggio finanziario riceve notifiche sui nuovi report, li controlla e li analizza).
  4. Casi leggermente più complessi riguardano, ad esempio, la generazione di una richiesta a Kubernetes, che crea un pod con i contenitori necessari, gli passa i parametri dell'attività e, dopo l'elaborazione, comprime il contenitore.

Ad esempio, creeremo una variante dell'attività 1, quando le modifiche nel bucket di storage di oggetti Mail.ru Cloud Solutions (MCS) vengono sincronizzate nello storage di oggetti AWS utilizzando webhook. In un caso di caricamento reale, il lavoro asincrono dovrebbe essere fornito registrando i webhook in coda, ma per l'attività di training eseguiremo l'implementazione senza di ciò.

Schema di lavoro

Il protocollo di interazione è descritto in dettaglio in Guida ai webhook S3 su MCS. Lo schema di lavoro contiene i seguenti elementi:

  • Servizio editoriale, che si trova sul lato storage S3 e pubblica richieste HTTP quando viene attivato il webnhook.
  • Server di ricezione del webhook, che ascolta le richieste dal servizio di pubblicazione HTTP ed esegue le azioni appropriate. Il server può essere scritto in qualsiasi lingua; nel nostro esempio scriveremo il server in Go.

Una particolarità dell'implementazione dei webhook nell'API S3 è la registrazione del server di ricezione del webhook sul servizio di pubblicazione. In particolare, il server ricevente del webhook deve confermare l'iscrizione ai messaggi dal servizio di pubblicazione (in altre implementazioni del webhook, solitamente non è richiesta la conferma dell'iscrizione).

Di conseguenza, il server di ricezione del webhook deve supportare due operazioni principali:

  • rispondere alla richiesta del servizio editoriale di confermare la registrazione,
  • elaborare gli eventi in arrivo.

Installazione di un server di ricezione webhook

Per eseguire il server di ricezione del webhook, è necessario un server Linux. In questo articolo, ad esempio, utilizziamo un'istanza virtuale che distribuiamo su MCS.

Installiamo il software necessario e avviamo il server di ricezione del webhook.

ubuntu@ubuntu-basic-1-2-10gb:~$ sudo apt-get install git
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  bc dns-root-data dnsmasq-base ebtables landscape-common liblxc-common 
liblxc1 libuv1 lxcfs lxd lxd-client python3-attr python3-automat 
python3-click python3-constantly python3-hyperlink
  python3-incremental python3-pam python3-pyasn1-modules 
python3-service-identity python3-twisted python3-twisted-bin 
python3-zope.interface uidmap xdelta3
Use 'sudo apt autoremove' to remove them.
Suggested packages:
  git-daemon-run | git-daemon-sysvinit git-doc git-el git-email git-gui 
gitk gitweb git-cvs git-mediawiki git-svn
The following NEW packages will be installed:
  git
0 upgraded, 1 newly installed, 0 to remove and 46 not upgraded.
Need to get 3915 kB of archives.
After this operation, 32.3 MB of additional disk space will be used.
Get:1 http://MS1.clouds.archive.ubuntu.com/ubuntu bionic-updates/main 
amd64 git amd64 1:2.17.1-1ubuntu0.7 [3915 kB]
Fetched 3915 kB in 1s (5639 kB/s)
Selecting previously unselected package git.
(Reading database ... 53932 files and directories currently installed.)
Preparing to unpack .../git_1%3a2.17.1-1ubuntu0.7_amd64.deb ...
Unpacking git (1:2.17.1-1ubuntu0.7) ...
Setting up git (1:2.17.1-1ubuntu0.7) ...

Clona la cartella con il server di ricezione del webhook:

ubuntu@ubuntu-basic-1-2-10gb:~$ git clone
https://github.com/RomanenkoDenys/s3-webhook.git
Cloning into 's3-webhook'...
remote: Enumerating objects: 48, done.
remote: Counting objects: 100% (48/48), done.
remote: Compressing objects: 100% (27/27), done.
remote: Total 114 (delta 20), reused 45 (delta 18), pack-reused 66
Receiving objects: 100% (114/114), 23.77 MiB | 20.25 MiB/s, done.
Resolving deltas: 100% (49/49), done.

Avviamo il server:

ubuntu@ubuntu-basic-1-2-10gb:~$ cd s3-webhook/
ubuntu@ubuntu-basic-1-2-10gb:~/s3-webhook$ sudo ./s3-webhook -port 80

Iscriviti al servizio di pubblicazione

Puoi registrare il tuo server di ricezione webhook tramite l'API o l'interfaccia web. Per semplicità, ci registreremo tramite l'interfaccia web:

  1. Andiamo alla sezione dei secchi nella sala di controllo.
  2. Vai al bucket per il quale configureremo i webhook e fai clic sull'ingranaggio:

Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions

Vai alla scheda Webhook e fai clic su Aggiungi:

Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions
Compila i campi:

Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions

ID: il nome del webhook.

Evento - quali eventi trasmettere. Abbiamo impostato la trasmissione di tutti gli eventi che si verificano quando si lavora con i file (aggiunta ed eliminazione).

URL: indirizzo del server di ricezione del webhook.

Il prefisso/suffisso del filtro è un filtro che consente di generare webhook solo per oggetti i cui nomi corrispondono a determinate regole. Ad esempio, affinché il webhook attivi solo i file con estensione .png, in Suffisso del filtro devi scrivere "png".

Attualmente, solo le porte 80 e 443 sono supportate per l'accesso al server di ricezione del webhook.

Facciamo clic Aggiungi gancio e vedremo quanto segue:

Un esempio di un'applicazione basata su eventi basata su webhook nello storage di oggetti S3 Mail.ru Cloud Solutions
Gancio aggiunto.

Il server di ricezione del webhook mostra nei suoi log l'avanzamento del processo di registrazione del webhook:

ubuntu@ubuntu-basic-1-2-10gb:~/s3-webhook$ sudo ./s3-webhook -port 80
2020/06/15 12:01:14 [POST] incoming HTTP request from 
95.163.216.92:42530
2020/06/15 12:01:14 Got timestamp: 2020-06-15T15:01:13+03:00 TopicArn: 
mcs5259999770|myfiles-ash|s3:ObjectCreated:*,s3:ObjectRemoved:* Token: 
E2itMqAMUVVZc51pUhFWSp13DoxezvRxkUh5P7LEuk1dEe9y URL: 
http://89.208.199.220/webhook
2020/06/15 12:01:14 Generate responce signature: 
3754ce36636f80dfd606c5254d64ecb2fd8d555c27962b70b4f759f32c76b66d

La registrazione è completata. Nella sezione successiva, daremo uno sguardo più da vicino all'algoritmo di funzionamento del server di ricezione del webhook.

Descrizione del server di ricezione del webhook

Nel nostro esempio, il server è scritto in Go. Diamo un'occhiata ai principi di base del suo funzionamento.

package main

// Generate hmac_sha256_hex
func HmacSha256hex(message string, secret string) string {
}

// Generate hmac_sha256
func HmacSha256(message string, secret string) string {
}

// Send subscription confirmation
func SubscriptionConfirmation(w http.ResponseWriter, req *http.Request, body []byte) {
}

// Send subscription confirmation
func GotRecords(w http.ResponseWriter, req *http.Request, body []byte) {
}

// Liveness probe
func Ping(w http.ResponseWriter, req *http.Request) {
    // log request
    log.Printf("[%s] incoming HTTP Ping request from %sn", req.Method, req.RemoteAddr)
    fmt.Fprintf(w, "Pongn")
}

//Webhook
func Webhook(w http.ResponseWriter, req *http.Request) {
}

func main() {

    // get command line args
    bindPort := flag.Int("port", 80, "number between 1-65535")
    bindAddr := flag.String("address", "", "ip address in dot format")
    flag.StringVar(&actionScript, "script", "", "external script to execute")
    flag.Parse()

    http.HandleFunc("/ping", Ping)
    http.HandleFunc("/webhook", Webhook)

log.Fatal(http.ListenAndServe(*bindAddr+":"+strconv.Itoa(*bindPort), nil))
}

Considera le funzioni principali:

  • Ping() - un percorso che risponde tramite URL/ping, l'implementazione più semplice di un'indagine di attività.
  • Webhook() - percorso principale, gestore URL/webhook:
    • conferma l'iscrizione al servizio editoriale (vai alla funzione Conferma Iscrizione),
    • elabora i webhook in entrata (funzione Gorecords).
  • Le funzioni HmacSha256 e HmacSha256hex sono implementazioni degli algoritmi di crittografia HMAC-SHA256 e HMAC-SHA256 con output come stringa di numeri esadecimali per il calcolo della firma.
  • main è la funzione principale, elabora i parametri della riga di comando e registra i gestori URL.

Parametri della riga di comando accettati dal server:

  • -port è la porta su cui il server ascolterà.
  • -address - Indirizzo IP che il server ascolterà.
  • -script è un programma esterno che viene chiamato per ogni hook in entrata.

Diamo uno sguardo più da vicino ad alcune delle funzioni:

//Webhook
func Webhook(w http.ResponseWriter, req *http.Request) {

    // Read body
    body, err := ioutil.ReadAll(req.Body)
    defer req.Body.Close()
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }

    // log request
    log.Printf("[%s] incoming HTTP request from %sn", req.Method, req.RemoteAddr)
    // check if we got subscription confirmation request
    if strings.Contains(string(body), 
""Type":"SubscriptionConfirmation"") {
        SubscriptionConfirmation(w, req, body)
    } else {
        GotRecords(w, req, body)
    }

}

Questa funzione determina se è arrivata una richiesta di conferma della registrazione oppure un webhook. Come segue da documentazione, se la registrazione viene confermata, nella richiesta Post viene ricevuta la seguente struttura Json:

POST http://test.com HTTP/1.1
x-amz-sns-messages-type: SubscriptionConfirmation
content-type: application/json

{
    "Timestamp":"2019-12-26T19:29:12+03:00",
    "Type":"SubscriptionConfirmation",
    "Message":"You have chosen to subscribe to the topic $topic. To confirm the subscription you need to response with calculated signature",
    "TopicArn":"mcs2883541269|bucketA|s3:ObjectCreated:Put",
    "SignatureVersion":1,
    "Token":«RPE5UuG94rGgBH6kHXN9FUPugFxj1hs2aUQc99btJp3E49tA»
}

È necessario rispondere a questa domanda:

content-type: application/json

{"signature":«ea3fce4bb15c6de4fec365d36bcebbc34ccddf54616d5ca12e1972f82b6d37af»}

Dove la firma viene calcolata come:

signature = hmac_sha256(url, hmac_sha256(TopicArn, 
hmac_sha256(Timestamp, Token)))

Se arriva un webhook, la struttura della richiesta Post è simile a questa:

POST <url> HTTP/1.1
x-amz-sns-messages-type: SubscriptionConfirmation

{ "Records":
    [
        {
            "s3": {
                "object": {
                    "eTag":"aed563ecafb4bcc5654c597a421547b2",
                    "sequencer":1577453615,
                    "key":"some-file-to-bucket",
                    "size":100
                },
            "configurationId":"1",
            "bucket": {
                "name": "bucketA",
                "ownerIdentity": {
                    "principalId":"mcs2883541269"}
                },
                "s3SchemaVersion":"1.0"
            },
            "eventVersion":"1.0",
            "requestParameters":{
                "sourceIPAddress":"185.6.245.156"
            },
            "userIdentity": {
                "principalId":"2407013e-cbc1-415f-9102-16fb9bd6946b"
            },
            "eventName":"s3:ObjectCreated:Put",
            "awsRegion":"ru-msk",
            "eventSource":"aws:s3",
            "responseElements": {
                "x-amz-request-id":"VGJR5rtJ"
            }
        }
    ]
}

Di conseguenza, a seconda della richiesta, è necessario capire come trattare i dati. Ho scelto la voce come indicatore "Type":"SubscriptionConfirmation", poiché è presente nella richiesta di conferma della sottoscrizione e non è presente nel webhook. In base alla presenza/assenza di questa voce nella richiesta POST, l'ulteriore esecuzione del programma va alla funzione SubscriptionConfirmationo in una funzione GotRecords.

Non considereremo nel dettaglio la funzione SubscriptionConfirmation; essa è implementata secondo i principi enunciati nell'art documentazione. È possibile visualizzare il codice sorgente per questa funzione su repository git del progetto.

La funzione GotRecords analizza una richiesta in arrivo e per ogni oggetto Record chiama uno script esterno (il cui nome è stato passato nel parametro -script) con i parametri:

  • nome del secchio
  • chiave dell'oggetto
  • azione:
    • copia - se nella richiesta originale EventName = ObjectCreated | MettiOggetto | PutObjectCopy
    • elimina - se nella richiesta originale EventName = ObjectRemoved | Eliminaoggetto

Pertanto, se arriva un hook con una richiesta Post, come descritto soprae il parametro -script=script.sh, lo script verrà chiamato come segue:

script.sh  bucketA some-file-to-bucket copy

Dovrebbe essere chiaro che questo server di ricezione webhook non è una soluzione di produzione completa, ma un esempio semplificato di una possibile implementazione.

Esempio di lavoro

Sincronizziamo i file dal bucket principale in MCS al bucket di backup in AWS. Il bucket principale si chiama myfiles-ash, quello di backup si chiama myfiles-backup (la configurazione del bucket in AWS va oltre lo scopo di questo articolo). Di conseguenza, quando un file viene inserito nel bucket principale, la sua copia dovrebbe apparire in quello di backup e quando viene eliminato da quello principale, dovrebbe essere eliminato in quello di backup.

Lavoreremo con i bucket utilizzando l'utilità awscli, che è compatibile sia con lo storage cloud MCS che con lo storage cloud AWS.

ubuntu@ubuntu-basic-1-2-10gb:~$ sudo apt-get install awscli
Reading package lists... Done
Building dependency tree
Reading state information... Done
After this operation, 34.4 MB of additional disk space will be used.
Unpacking awscli (1.14.44-1ubuntu1) ...
Setting up awscli (1.14.44-1ubuntu1) ...

Configuriamo l'accesso all'API S3 MCS:

ubuntu@ubuntu-basic-1-2-10gb:~$ aws configure --profile mcs
AWS Access Key ID [None]: hdywEPtuuJTExxxxxxxxxxxxxx
AWS Secret Access Key [None]: hDz3SgxKwXoxxxxxxxxxxxxxxxxxx
Default region name [None]:
Default output format [None]:

Configuriamo l'accesso all'API AWS S3:

ubuntu@ubuntu-basic-1-2-10gb:~$ aws configure --profile aws
AWS Access Key ID [None]: AKIAJXXXXXXXXXXXX
AWS Secret Access Key [None]: dfuerphOLQwu0CreP5Z8l5fuXXXXXXXXXXXXXXXX
Default region name [None]:
Default output format [None]:

Verifichiamo gli accessi:

Ad AWS:

ubuntu@ubuntu-basic-1-2-10gb:~$ aws s3 ls --profile aws
2020-07-06 08:44:11 myfiles-backup

Per MCS, quando si esegue il comando è necessario aggiungere —endpoint-url:

ubuntu@ubuntu-basic-1-2-10gb:~$ aws s3 ls --profile mcs --endpoint-url 
https://hb.bizmrg.com
2020-02-04 06:38:05 databasebackups-0cdaaa6402d4424e9676c75a720afa85
2020-05-27 10:08:33 myfiles-ash

Accesso effettuato.

Ora scriviamo uno script per elaborare l'hook in entrata, chiamiamolo s3_backup_mcs_aws.sh

#!/bin/bash
# Require aws cli
# if file added — copy it to backup bucket
# if file removed — remove it from backup bucket
# Variables
ENDPOINT_MCS="https://hb.bizmrg.com"
AWSCLI_MCS=`which aws`" --endpoint-url ${ENDPOINT_MCS} --profile mcs s3"
AWSCLI_AWS=`which aws`" --profile aws s3"
BACKUP_BUCKET="myfiles-backup"

SOURCE_BUCKET=""
SOURCE_FILE=""
ACTION=""

SOURCE="s3://${SOURCE_BUCKET}/${SOURCE_FILE}"
TARGET="s3://${BACKUP_BUCKET}/${SOURCE_FILE}"
TEMP="/tmp/${SOURCE_BUCKET}/${SOURCE_FILE}"

case ${ACTION} in
    "copy")
    ${AWSCLI_MCS} cp "${SOURCE}" "${TEMP}"
    ${AWSCLI_AWS} cp "${TEMP}" "${TARGET}"
    rm ${TEMP}
    ;;

    "delete")
    ${AWSCLI_AWS} rm ${TARGET}
    ;;

    *)
    echo "Usage: 
#!/bin/bash
# Require aws cli
# if file added — copy it to backup bucket
# if file removed — remove it from backup bucket
# Variables
ENDPOINT_MCS="https://hb.bizmrg.com"
AWSCLI_MCS=`which aws`" --endpoint-url ${ENDPOINT_MCS} --profile mcs s3"
AWSCLI_AWS=`which aws`" --profile aws s3"
BACKUP_BUCKET="myfiles-backup"
SOURCE_BUCKET="${1}"
SOURCE_FILE="${2}"
ACTION="${3}"
SOURCE="s3://${SOURCE_BUCKET}/${SOURCE_FILE}"
TARGET="s3://${BACKUP_BUCKET}/${SOURCE_FILE}"
TEMP="/tmp/${SOURCE_BUCKET}/${SOURCE_FILE}"
case ${ACTION} in
"copy")
${AWSCLI_MCS} cp "${SOURCE}" "${TEMP}"
${AWSCLI_AWS} cp "${TEMP}" "${TARGET}"
rm ${TEMP}
;;
"delete")
${AWSCLI_AWS} rm ${TARGET}
;;
*)
echo "Usage: ${0} sourcebucket sourcefile copy/delete"
exit 1
;;
esac
sourcebucket sourcefile copy/delete" exit 1 ;; esac

Avviamo il server:

ubuntu@ubuntu-basic-1-2-10gb:~/s3-webhook$ sudo ./s3-webhook -port 80 -
script scripts/s3_backup_mcs_aws.sh

Vediamo come funziona. Attraverso Interfaccia web MCS aggiungi il file test.txt al bucket myfiles-ash. I log della console mostrano che è stata effettuata una richiesta al server webhook:

2020/07/06 09:43:08 [POST] incoming HTTP request from 
95.163.216.92:56612
download: s3://myfiles-ash/test.txt to ../../../tmp/myfiles-ash/test.txt
upload: ../../../tmp/myfiles-ash/test.txt to 
s3://myfiles-backup/test.txt

Controlliamo il contenuto del bucket myfiles-backup in AWS:

ubuntu@ubuntu-basic-1-2-10gb:~/s3-webhook$ aws s3 --profile aws ls 
myfiles-backup
2020-07-06 09:43:10       1104 test.txt

Ora, tramite l'interfaccia web, elimineremo il file dal bucket myfiles-ash.

Registri del server:

2020/07/06 09:44:46 [POST] incoming HTTP request from 
95.163.216.92:58224
delete: s3://myfiles-backup/test.txt

Contenuto del secchio:

ubuntu@ubuntu-basic-1-2-10gb:~/s3-webhook$ aws s3 --profile aws ls 
myfiles-backup
ubuntu@ubuntu-basic-1-2-10gb:~$

Il file viene eliminato, il problema è risolto.

Conclusione e cose da fare

Tutto il codice utilizzato in questo articolo è nel mio archivio. Sono inoltre disponibili esempi di script ed esempi di conteggio delle firme per la registrazione dei webhook.

Questo codice non è altro che un esempio di come puoi utilizzare i webhook S3 nelle tue attività. Come ho detto all'inizio, se prevedi di utilizzare un server di questo tipo in produzione, devi almeno riscrivere il server per il lavoro asincrono: registrare i webhook in entrata in una coda (RabbitMQ o NATS), e da lì analizzarli ed elaborarli con le domande dei lavoratori. Altrimenti, quando i webhook arrivano in massa, potresti riscontrare una mancanza di risorse del server per completare le attività. La presenza di code consente di distribuire server e lavoratori, nonché di risolvere problemi con la ripetizione delle attività in caso di guasti. Si consiglia inoltre di modificare la registrazione in una più dettagliata e standardizzata.

Buona fortuna!

Ulteriori letture sull'argomento:

Fonte: habr.com

Aggiungi un commento