Alpha Lambda Omega (Linux-Magazin, März 2017)

Auf Amazons "Lambda"-Service laufen selbstgeschriebene Python-Skripts in Container-Umgebungen, demonstriert im heutigen Programming Snapshot am Beispiel eines AI-Programms zur Bewegungsanalyse in Überwachungsvideos.

Den Babyschritten in der letzten Ausgabe zum Einrichten eines AWS-Accounts, einer S3-Storage mit statischem Webserver, sowie der ersten Lambda-Funktion folgt heute als Schlussspurt der Setup eines API-Servers auf Amazon zum Aufstöbern von interessanten Szenen in Aufnahmen einer Überwachungskamera. Die per Web-Aufruf, entweder vom Browser oder von einem Kommandozeilen-Tool wie curl getriggerte Lambda-Funktion holt hierzu ein Video vom Netz, jagt es durch einen mittels der OpenCV-Library implementierten AI-Algorithmus, erzeugt daraus ein Bewegungsprofil und gibt den URL eines als JPEG-Datei generierten Kontaktabzugs mit den wichtigsten Videobewegungen zurück (Abbildung 1 und 2).

Sandkastenspiele

Im Gegensatz zu Amazons EC2-Instanzen mit ihren vollblütigen (wenngleich auch nur virtuellen) Linux-Servern bietet der Lambda-Service nur eine containerisierte Umgebung. In ihr laufen NodeJS, Python-, oder Java-basierte Lösungen in einem Sandkasten, den Amazon nach Belieben zwischen echten Servern herumschubst oder bei Inaktivität gar ganz webputzt, um ihn erst später beim nächsten Zugriff wieder hervorzuzaubern. Daten auf der virtuellen Festplatte des Containers liegen zu lassen und darauf zu hoffen, dass sie beim nächsten Aufruf noch da sind, ergäbe also eine extrem instabile Applikation. Statt dessen kommunizieren Lambda-Funktionen mit anderen AWS-Angeboten wie der S3-Storage oder der Dynamo-Datenbank um Daten zu sichern und agieren ansonsten "stateless".

Was eine Applikation nicht in einem Python-Skript beschreiben kann, darf der Entwickler allerdings auch als .zip-Datei in den auf, so munkelt man, Cent-OS basierenden Container hochladen. Eine Lambda-Funktion, die wie im vorliegenden Fall Artificial-Intelligence-Funktionen aus der OpenCV-Library nutzt, muss die notwendigen Binaries oder Libraries vorher in einer dem Lambda-Container ähnlichen Unix-Umgebung kompilieren, verpacken, hochladen, und später zur Laufzeit aus dem Python-Skript aus aufrufen. Dabei kommen entweder verfügbare Python-Bindings zu shared Libraries zum Einsatz oder das Python-Skript ruft vorkompilierte Binaries als externen Prozess auf.

Abbildung 1: Das AI-Programm zur Bewegungsanalyse läuft auf einem Amazon-Server hinter einer REST-API.

Abbildung 2: Der auf AWS erzeugte Kontaktabzug zeigt die Sekunden im Überwachungsvideo, in denen sich tatsächlich etwas bewegt hat.

Rank und Schlank

Damit das AI-Programm aus [2] nach der Installation in der Amazon-Cloud nicht zuviel Rechenzeit und nach dem Verlassen des kostenlosen Plans "Free Tier" auch Geld verbrät, sucht der Code in der gegenüber der letzten Ausgabe verbesserten Version in Listing 1 nun nicht mehr in jedem Frame, also 50 mal pro Sekunde, nach Bewegungen, sondern hüpft in Zeile 99 nun in Schritten von einer halben Sekunde durch den Film. Nach einem gefundenen Frame mit Bewegung springt Zeile 96 gar zwei Sekunden (zweimal die Frames/Sekunde-Rate in fps) vorwärts. Im Gegensatz zu vid.read() dekodiert das in Zeile 50 aufgerufene vid.grab() nicht mehr aufwändig, sondern wirft ihn einfach weg, um zum nächsten zu gelangen.

Und während die erste Version in [2] nur die Sekundenwerte im Video in die Ausgabe schrieb, an denen der Algorithmus Bewegungen erkannte, um nachfolgend über Tausendsassa mplayer die zugehörigen Frames als JPEG-Dateien zu extrahieren, schreiben die Zeilen 92-94 erkannte Frames gleich mittels der OpenCV beiliegenden Bildverarbeitungsfunktionen imwrite() im Format 0001.jpg auf die virtuelle Festplatte. Ein zweiter Durchlauf, sowie die Frickelei zur Installation von mplayer in den Lambda-Container entfallen somit.

Listing 1: max-movement-lk.cpp

    048 int frames_skip( VideoCapture vid, int n, int *i ) {
    049     for( int c = 0; c < n; c++ ) {
    050       if (!vid.grab())
    051         break;
    052       (*i)++;
    053     }
    054 }
    ...
    056 int main(int argc, char *argv[]) {
    ...
    080   while (1) {
    081     if (!vid.read(frame))
    082       break;
    083     i++;
    084 
    085     int movie_second = i / fps;
    086 
    087     cframe = frame.clone();
    088     cvtColor(frame,frame,COLOR_BGR2GRAY);
    089     if(move_test(oframe, frame)) {
    090       cout << movie_second << "\n";
    091       
    092       char filename[80];
    093       sprintf( filename, "%04d.jpg", i/fps );
    094       imwrite( filename, cframe );
    095 
    096       frames_skip( vid, 2*fps, &i );
    097     } else {
    098         // fast-forward to next 1/2 sec
    099       frames_skip( vid, fps/2, &i );
    100     }
    101 
    102     oframe = frame;
    103   }
    ...

Aus diesen JPEG-Bildern macht dann ein weiteres Python-Skript, mk-montage.py, unter Zuhilfenahme der Image-Magick-Library einen Kontaktabzug, ebenfalls im JPEG-Format. Diese Datei legt das Lambda-Programm dann in Amazons S3-Cloud-Speicher ab und schickt eine URL darauf an den aufrufenden Client zurück.

RAM ist Geld

Wie holt ein Python-Programmierer ein Dokument vom Web? Ein Ansatz wäre die Methode read() nach einem urlopen(), die alle eingeholten Bytes gleich wieder mit write() in eine lokale Datei schreibt, aber das hätte zur Folge, dass eine eventuell große Videodatei komplett ins RAM gelesen würde, bevor Python sie auf die Platte schriebe. RAM kostet Geld auf Amazon, also verwendet Listing 2 die Methode urlretrieve aus dem Modul urllib, die (hoffentlich) mehr oder weniger intelligent stückweise puffert.

Zwiespalt Python 2.x und 3

Die Python-Welt leidet unter dem Zwiespalt zwischen Python 2.x und 3. Letzteres ist eine Art Paradieszustand, in dem Kinderkrankheiten behoben, Unstimmigkeiten bereinigt sind, und in dem alle coolen Neuentwicklungen stattfinden. Allerdings nutzt kaum jemand Python 3 in Produktionsumgebungen und auch Amazon bietet auf AWS nur 2.7 an. In Python 2.x schlägt sich der Programmierer dann mit hanebüchenem Wildwuchs an Libraries herum, und muss sich zum Beispiel beim Einholen von Webdaten zwischen den zwei inkompatiblen Erzeugnissen urllib und, kein Scherz, urllib2 entscheiden. Oder wer externe Programme starten möchte, nutzt in 2.x check_output() des Moduls subprocess>, während in Python 3.x die Methode run() mit anderen Parametern zum Einsatz kommt und check_output() nicht mal mehr existiert.

Lambda Go

Die Lambda-Funktion in Listing 2 bekommt den URL der zu analysierenden Video-Datei im Parameter-Dictionary event unter dem Schlüssel movie_url zugespielt.

Listing 2: vimo.py

    01 #!/usr/bin/python
    02 import urllib
    03 import tempfile
    04 import shutil
    05 import subprocess
    06 import boto3
    07 import os
    08 
    09 def lambda_handler(event, context):
    10     tmpd = tempfile.mkdtemp()
    11 
    12     # fetch movie
    13     movie_url  = event['movie_url']
    14     movie_file = os.path.join(tmpd,
    15         os.path.basename(movie_url))
    16     urllib.urlretrieve(movie_url,movie_file)
    17 
    18     # motion analysis
    19     print subprocess.check_output([
    20         "bin/max-movement-lk.py", 
    21         movie_file])
    22 
    23     # generate montage
    24     print subprocess.check_output([
    25         "bin/mk-montage.py",tmpd])
    26 
    27      # store montage in s3
    28     s3 = boto3.resource('s3')
    29     bucket = "snapshot.linux-magazin.de"
    30     data = open(os.path.join(
    31          tmpd,'montage.jpg')).read()
    32     s3.Bucket(bucket).put_object(
    33         Key="montage.jpg",
    34         Body=data,ContentType="image/jpeg")
    35 
    36     result = { "montage_url": 
    37       "https://s3-us-west-2.amazonaws.com" +
    38       "/snapshot.linux-magazin.de/" +
    39       "montage.jpg"}
    40 
    41     shutil.rmtree(tmpd)
    42     return result

In einer echten Produktionsumgebung darf kein Python-Skript in einem festen Verzeichnis wie data operieren und hoffen, dass niemand dazwischenfunkt. Da Amazon-Lambda-Funktionen parallel aufgerufen werden, müssen sie für solche Zwecke mit Pythons tempfile-Modul zunächst ein instanzeigenes temporäres Verzeichnis anlegen, und nach Abschluss der Tätigkeit wieder abräumen.

Damit dies auch passiert, falls eine der vorher aufgerufenen Funktionen auf einen Fehler läuft und eine Exception wirft, sollte der letzte Zeile im Produktionsbetrieb eigentlich in einem Exception-Handler stehen, das unterblieb in der Testversion. Listing 2 ruft in Zeile 10 die Methode mkdtemp() auf und nutzt das neu angelegte Verzeichnis um in Zwischenschritten ermittelte Daten für die nächsten Stufen des Skripts abzulegen.

So legt Zeile 16 die per Webrequest eingeholte Videodatei unter dem in der Variable movie_file abgelegten Namen ab, der aus dem letzten Teil des Pfads der URL stammt. Als nächste Stufe ruft Zeile 19 das Skript max-movement-lk.py in Listing 3 auf, einen Python-Wrapper um das C++-Programm in Listing 1, und übergibt ihm den Pfad zur Videodatei im temporären Verzeichnis.

Listing 3: max-movement-lk.py

    01 #!/usr/bin/python
    02 import sys
    03 import os
    04 import subprocess
    05 
    06 top_dir    = os.getcwd()
    07 movie_path = sys.argv[1]
    08 
    09 os.chdir(os.path.dirname(movie_path))
    10 
    11 os.environ["LD_LIBRARY_PATH"] = os.path.join(top_dir,"lib")
    12 
    13 print subprocess.check_output(
    14   [ os.path.join(top_dir, "bin/max-movement-lk") ] + 
    15   [ os.path.basename(movie_path) ] )

Auf Montage

Hinterlässt die Bewegungsanalyse dort nun eine Reihe von JPEG-Dateien im Format 0001.jpg, 0002.jpg ..., kommt in der nächsten Verarbeitungsstufe ab Zeile 24 in Listing 2 das Wrapper-Skript mk-montage.py in Listing 4 zum Einsatz, das den in die Dateinamen eingebetteten Sekundenwerte ins Format SS::MM:ss umwandelt und die alten Dateinamen sowie die formatierten Labels Imagemagicks montage zu fressen gibt:


    montage.py -label 00:00:01 tmp/001.jpg ...

Das Programm baut daraus einen Kontaktabzug in der Datei montage.jpg, die später in Amazons S3 gespeichert wird, damit User sie per Web-Link auf den Client holen können.

Listing 4: mk-montage.py

    01 #!/usr/bin/python
    02 import glob
    03 import subprocess
    04 import re
    05 import time
    06 import os
    07 import sys
    08 
    09 dir = sys.argv[1]
    10 files = glob.glob(
    11 	  os.path.join(dir,'*.jpg'))
    12 cmds  = ["bin/montage.py"]
    13 
    14 r = re.compile('.*?(\d+)\.jpg')
    15 
    16 for file in sorted(files):
    17     match = r.match(file)
    18     if match:
    19         label = time.strftime("%H:%M:%S", 
    20           time.gmtime(int(match.group(1))))
    21         cmds.append("-label")
    22         cmds.append(label)
    23         cmds.append(file)
    24     else:
    25         print "no match: " + file
    26 
    27 cmds.append(os.path.join(
    28     dir,'montage.jpg'))
    29 
    30 print subprocess.check_output(cmds)

Das Python-Skript in Listing 5 fungiert als Wrapper um das Binary montage, dem im Verzeichnis lib eine Reihe von shared Libraries beiligen, damit das dynamisch gelinkte Binary im Container läuft. Die Environment-Variable LD_LIBRARY_PATH setzt den Suchpfad für shared Libs auf dieses nicht standardisierte Verzeichnis, damit das Binary diese zur Laufzeit auch findet.

Listing 5: montage.py

    1 #!/usr/bin/python
    2 import sys
    3 import os
    4 import subprocess
    5 
    6 os.environ["LD_LIBRARY_PATH"] = "lib"
    7 
    8 print subprocess.check_output(
    9   [ "bin/montage" ] + sys.argv[1:])

Die Frickelei, alle von einem Binary genutzten shared Libraries in ein Verzeichnis zum späteren Bündeln zu kopieren übernimmt das Python-Skript in Listing 6. In einer kontrollierte Umgebung wie einem Docker-Container oder einer Vagrant-VM, die dem Zielsystem (also Cent-OS) möglichst nahe ist, installiert der Admin hierzu das gewünschte Binary als Paket der verwendeten Distribution mittels yum, lässt das Skript dann mit dem Unix-Tool ldd die verwendeten shared Libs ermitteln und sammelt diese in einem neu angelegten Verzeichnis:

    for i in `ldd-ls.py program`
      do cp $i /build/libs
    done

Diese Sammlung assistierender Libs zum Binary wird später in einem gezippten Archiv auf Amazons Lambda-Service hochgeladen und steht damit der Lambda-Funktion im Container zur Verfügung. Dabei bietet die AWS-Konsole sowohl die Möglichkeit eines direkten File-Uploads im Browser als auch den Zugriff auf einen S3-Bucket, auf den der Admin die Zip-Datei vorher per Kommandozeilen-Tool aws hochgeladen hat (Abbildung 3).

Abbildung 3: Hochladen der Zip-Datei auf den Lambda-Server über einen Amazon-S3-Bucket.

Listing 6: ldd-ls.py

    01 #!/usr/bin/python
    02 import subprocess;
    03 import sys;
    04 
    05 if len(sys.argv) != 2:
    06     print("usage: {} file".format(sys.argv[0]))
    07     sys.exit(1)
    08     
    09 file = sys.argv[1]
    10 
    11 output = subprocess.check_output(['ldd',file])
    12 for line in output.split("\n"):
    13     words = line.split()
    14     if len(words) > 3:
    15         print words[2]

Abbildung 4: Libs

Abbildung 4 zeigt die so gesammelten shared Libs, offensichtlich zieht das mit OpenCV gelinkte AI-Programm zur Bewegungsanalyse einen ganzen Rattenschwanz an Bibliotheken hinter sich her. Schließlich steckt hinter einem Video geballte Compressionstechnik, die es zu dekodieren gilt, wenn das Programm an die rohen Framedaten heran möchte.

Sicher abgelegt

Ist der Kontakabzug montage.jpg erstellt, kopiert der Code ab Zeile 28 in Listing 2 die Datei aus dem temporären Verzeichnis in einen vorher angelegten S3-Bucket auf Amazons Cloud-Storage-System. Das Python-Modul boto3 steht auf Lambda-Servern standardmäßig zur Verfügung und bietet allerlei Tools zur Kommunikation mit verwandten Serviceangeboten. Die Methode put_object in Zeile 32 legt die von der virtuellen Festplatte gelesene Ausgabedatei als Objekt vom Typ image/jpeg im Cloudspeicher ab. Von dort liefert sie der in der letzten Ausgabe besprochene S3-Webserver an den interessierten User aus, dem der API-Aufruf nach Abschluß den zugehörigen URL gesteckt hat. Damit dieser sie auch findet, stellt Zeile 36 eine JSON-Antwort zusammen, die dem Web-Client den zur Montage-Datei zugehörigen S3-URL mitteilt. Am Ende der Lambda-Funktion in Zeile 41 bleibt nur noch, das temporär angelegte Verzeichnis wieder zu löschen.

Damit das Lambda-Skript Schreibrechte an dem als snapshot.linux-magazin.de konfigurierten S3-Bucket erhält, muss der User letzterem entsprechende Rechte verleihen. Abbildung 5 zeigt, dass der S3-Bucket jedem ausgewiesenen AWS-User Zugriff gewährt. Auf der anderen Seite müssen vom Lambda-Server im S3-Bucket erzeugte Dateien auch weltweit für interessierte User lesbar sein. Dies erfolgt über eine sogenannte Bucket-Policy, deren Inhalt Listing 7 zeigt. Jede dort neu erzeugte Datei ist demnach für alle lesbar, also kann der am S3-Bucket hängende Webserver sie auch an anfragende Webclients ausliefern.

Abbildung 5: Der Lamda-Server benötigt Zugriffsrechte am S3-Bucket.

Listing 7: bucket-policy.json

    01 {
    02   "Version": "2012-10-17",
    03   "Statement": [
    04   {
    05       "Sid": "",
    06       "Effect": "Allow",
    07       "Principal": "*",
    08       "Action": "s3:GetObject",
    09       "Resource": "arn:aws:s3:::snapshot.linux-magazin.de/*"
    10   }
    11   ]
    12 }

Tor zur Welt

Amazon hilft beim Testen von Lambda-Funktionen, der Entwickler kann hochgeladene Skripts entweder durch die Kommandozeilen-Utilty aws oder auch den Test-Button der Console im Browser ausführen. Aber schließlich sollen User die Funktion letztlich aus dem offenen Internet ausführen können und hierzu bietet sich Amazons API-Gateway an. Dieser ebenfalls auf der Console anklickbare Service legt einen Cloud-Webserver mit einer REST-API an, deren Methoden (wie zum Beispiel /vimo im vorliegenden Beispiel) es neben anderen Optionen auf User-definierte Lambda-Funktionen umleitet.

Abbildung 6: POST Method

Die Verbindung zwischen Webserver und Applikation auf dem Lambda-Service erledigt Amazon AWS hinter den Kulissen ohne viel Heckmeck, wenn der User für die Option "Integration Type" beim Anlegen der REST-Methode (zum Beispiel GET oder POST) die Option "Lambda Function" angibt und weiter unten die Region des Datencenters (z.B. "us-west-2") und den Namen der Lambda-Funktion (z.B. "vimo") angibt.

Im vorliegenden Fall soll der Pfad /vimo die POST-Methode verwenden und im Body des Requests einen JSON-Blob mit benamten Parametern (z.B. "movie_url") führen. Setzt der Web-Client wie in Abbildung 1 sichtbar den Header "Content-Type" auf "application/json", dann fängt bereits das API-Gateway den JSON-Blob ab und analysiert ihn. Die später aufgerufene Lambda-Funktion erhält dann bereits die dekodierten Wertepaare aus den JSON-Daten in einem Python-Dictionary als Funktionsparameter event. Im vorliegenden Fall legt der Client in Abbildung 1 den URL zum Überwachungsvideo im JSON-Blob im Parameter movie_url ab, während die Lambda-Funktion in Listing 2 mit event['movie_url'] darauf zugreift.

Abbildung 7: Deploy

Live geschaltet wird die REST-API erst nachdem der User im Kontextmenü unter "API Actions" die Funktion Deploy API angeklickt und eine Produktionsumgebung ("Stage") ausgewählt hat (z.B. "Beta"). Im Browser zeigt AWS dann die URL an, unter der der neue Webservice erreichbar ist.

Abbildung 8: Dank Amazons Free Tier halten sich die Kosten für den Lambda-Service im Rahmen.

Als Produktionsumgebung installiert empfiehlt sich der Einsatz von API-Tokens, mit denen sich der Zugang zur API regeln lassen. Auch ein Drosseln des Ansturms (auf z.B. 1000 Requests/Sekunde) ist hiermit möglich, um einer überraschenden Kostenexplosion vorzubeugen, falls der Link sich lauffeuerartig verbreitet. Während der Entwicklung dieses Artikels hatte ich immer ein wachsames Auge auf eventuell anfallende Kosten, fand aber, dass diese sich im "Free Tier" im Rahmen hielten, es fielen lediglich Kosten von $0.01 an, um die während zahlreicher Testdurchgänge aufgebrauchte Bandbreite zum Hochladen der ständig aktualisierten und verbesserten .zip-Datei mit Testcode und Libs zu decken.

Infos

[1]

Listings zu diesem Artikel: http://www.linux‐magazin.de/static/listings/magazin/2017/03/perl‐snapshot

[2]

"Schaut auf diese Stadt", Bewegungsanalyse in Überwachungsvideos, Linux-Magazin 12/2016: http://www.linux-magazin.de/Ausgaben/2016/12/Perl-Snapshot

[3]

"AWS Lambda in Action", Danilo Poccia, Manning 2017

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.