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).
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. |
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.
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.
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.
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.
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.
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.
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) ] )
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.
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.
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. |
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.
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. |
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 }
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.
Listings zu diesem Artikel: http://www.linux‐magazin.de/static/listings/magazin/2017/03/perl‐snapshot
"Schaut auf diese Stadt", Bewegungsanalyse in Überwachungsvideos, Linux-Magazin 12/2016: http://www.linux-magazin.de/Ausgaben/2016/12/Perl-Snapshot
"AWS Lambda in Action", Danilo Poccia, Manning 2017