Freden begynder så småt at indfinde sig på Version2’s kode-redaktion. Det lille mødelokale på fjerde sal er fyldt til loftet med gaver, og it-supporten går rundt med nissehuer på hovedet.
Men som i alle gode historier skulle der overvindes nogle forhindringer på vejen.
I sidste kapitel af den tilsyneladende uendelige historie om Version2’s emneords-gætteri med kunstig intelligens, gik vi serverless, for at slippe for server-vedligeholdelse, bash-scripts og eksistentiel angst for sikkerhedshuller.
Vi fik et ‘hallo verden’-program op at køre i Amazons sky, efter at have installeret en hulens bunke kommando-linje-programmer. Denne gang skal vi have vores kunstige intelligens syet ind i de rammer, som serverless - kaldet Lambda - tilbyder.
Som vi så sidst, har vi allerede fået en masse fordele. Som nævnt dengang skal jeg ikke længere vedligeholde en Linux-server, og http-forbindelsen klarer serverless-miljøet for mig, og det samme gælder certifikater til SSL.
I sin tid skrev jeg en klasse i Java, Tester, som indlæser de afledte ordhyppigheder, eller estimater, der benyttes til at beregne, om et emneord skal accepteres eller forkastes, på baggrund af ordene i en spritny artikel på Version2.
Jeg er ganske grøn i forhold til Amazons serverless, så jeg gør det, der virker nemmest: Ser, om jeg ikke kan fuske lidt med ‘hallo verden’-appen, så den producerer emneords-bud.
Det gør jeg med venlig hjælp fra cloud-konsulent Henrik Oddershede, der fik sat mig i sving med serverless i den sidste artikel. Henrik har faktisk lavet et eksempel, der virker, men jeg skal jo finde ud af det på egen hånd, ellers er det lidt snyd i forhold til fortællingen - det synes jeg da, i det mindste.
Apps uden filsystem
Jeg bruger dog noget af Henriks kode, det må jeg tilstå. Det drejer sig netop om tabellerne med estimater, der fylder 126 megabyte. På den gamle server indlæste jeg dem som en fil, på helt almindelig vis. Men her i serverless er der ikke noget filsystem, så vi skal gøre noget andet.
Amazons serverless-api kan læse objekter fra storage-systemet S3, så jeg uploader estimaterne til min ‘bucket’, som det hedder, med et klik på en knap i Amazons web-konsol. Det kan næppe blive nemmere.
Estimat-filen kan nemt indlæses i serverless-app’en på følgende vis - og det er altså Henrik, der har skrevet koden - jeg vil ikke pynte mig med lånte fjer. Jeg har dog ændret lidt i koden til mit formål.
public static DiskTraener.Estimater getEstimater() throws IOException, ClassNotFoundException { System.out.println("Læser estimat-fil fra s3"); AmazonS3 s3 = AmazonS3ClientBuilder.standard().build(); S3Object objectInputStream = s3.getObject( "taniasnyebucket", "estimater.ser"); ObjectInputStream in = new ObjectInputStream( new BufferedInputStream(objectInputStream.getObjectContent())); DiskTraener.Estimater estimater = (DiskTraener.Estimater) in.readObject(); in.close(); System.out.println("Læste estimat-fil fra s3"); return estimater; }
Det foregår i Tester-klassen, der finder frem til de rigtige emneord - eller laver ‘inferens’, som det fint hedder på kunstig intelligens-sprog.
Det vil ikke
Men det virker dæleme ikke. Den vil ikke compile.
December-vejret er tungt og gråt her på Kalvebod Brygge i København, og jeg føler mig ikke så intelligent denne dag. Jeg ringer til min livlinje.
»Hej Henrik! Den vil ikke compile!«
»Har du tjekket, at det er den rigtige package,« lyder det venlige svar fra Henrik.
Øh nej, det har jeg vist ikke, der står package helloworld
i toppen af App.java
-kildefilen, og ikke package v2ml
.
Total pinlig begynderfejl. Det er også det dumme vejrs skyld. Sådan ser jeg på det.
Jeg ringer af og får rettet min copy-paste-fejl.
Efter at have rettet et par andre småfejl, compiler min lille app med den gamle Tester-kode. Amazons serverless-miljø sætter unittest op som standard, når ‘hello world’-skelettet genereres. Testen udføres, når der buildes med kommandoen sam build.
Det er smart, for jeg kan teste, at det virker, med assert-sætningen
assertTrue(content.contains("sundheds-it"));
Henrik har hardcodet en test-streng som input i sin kode, som ser sådan ud: sundhed ambulance hospital sikkerhed
og den skal blandt andre returnere emneordet 'sundheds-it', som er det jeg tester i assert-kaldet ovenfor.
Unittesten passerer - det virker! Jeg laver en triumferende springdans bag mit skrivebord.
Lokal succes
Men det er altså kun succes på min lokale maskine. Skyen venter lidt endnu.
Jeg har et lille problem. Min kode benytter faciliteter fra Java 9, og Amazons serverless-miljø kører Java 8. Henrik har i sit eksempel løst det ved - hold nu fast - at dekompilere en class-fil, fra det tidligere Version2-eksempel, hvor kildekoden ikke var med, og rettet min kode til Java 8. Temmelig imponerende.
Nu skete der det, i disse dage, som der står i evangeliet, at Amazon tilføjede Java 11 til sit serverless-miljø. Det løser alle problemer.
Jeg prøver at oprette et nyt Java 11-projekt med
sam init --runtime java11 -n v2mlv2 Usage: sam init [OPTIONS] Try "sam init -h" for help.
der svarer:
Error: Invalid value for "-r" / "--runtime": invalid choice: java11.
– så den gik ikke.
Jeg må spørge sagkundskaben igen. Denne gang sender jeg en mail - jeg synes ikke jeg kan forstyrre Henrik hele tiden med mine dumme spørgsmål.
Han forklarer, at jeg skal opdatere SAM-kommandoen:
brew tap aws/tap brew upgrade aws-sam-cli
Jeg tester versionen:
sam --version SAM CLI, version 0.37.0
Og det er den nyeste. »Men du behøver ikke at starte et nyt projekt for at skifte til java11. Du kan nøjes med at redigere din eksisterende template.yaml,« skriver Henrik i sin mail.
Template.yaml er en konfigurationsfil. Den ligger i roden af projektmappen. Inde i den finder jeg punktet Runtime
, der er sat til java8. Det ændrer jeg til java11:
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: HelloWorldFunction Handler: v2ml.App::handleRequest Runtime: java11
Henrik fortæller mig også, hvor jeg kan finde logfilen for funktionen. Det gøres ved at logge ind på AWS-konsollen. Under punktet ‘Find Services’ skriver jeg ‘lambda’ og trykker på det øverste resultat, der fører mig til en liste over mine funktioner.
Jeg har kun den ene, og den klikker jeg på. Så klikker jeg på fanen ‘Monitoring’ og derefter på knappen ‘View logs in CloudWatch’. Nu kommer logfilen frem, hvor jeg for eksempel kan se exceptions, hvis funktionen crasher. Hvis jeg skriver med System.out.println, er det også her, meddelelserne bliver printet.
Emneord med sky
Nu builder jeg projektet igen, og pakker og udruller. Den kompilerer fint, men crasher lige på stedet, når jeg kalder funktionen via browseren. Jeg ved godt, hvad det skyldes. Det er det der med hukommelsen, som drillede så meget, da jeg kørte programmet på sky-instansen, hvor der kun var 1 gigabyte til rådighed.
I Amazons serverless kan man allokere op til 3 gigabyte, uanset om det er gratis eller betalt. Lige nu står hukommelsen på 512 MB i template.yaml. Det ændrer jeg til 2048:
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: HelloWorldFunction Handler: v2ml.App::handleRequest Runtime: java11 MemorySize: 2048
Endnu en gang build, package og deploy, og så ind i browseren.
Så kører funktionen - herligt, herligt. Men der er endnu et punkt på dagsorden. For nemheds skyld har Henrik hardcodet den test-streng, vi så tidligere, som inddata, og som giver de ovenstående emneord som resultat. Det skal lige rigges til, så inddata kommer fra en post-request, fra den webudvidelse, der syer gættemaskinen sammen med vores cms.
Når jeg sender en forespørgsel via post, med curl-kommandoen i bash:
curl -s -d "java java java java" -X POST https://x8d598jn85.execute-api.us-east-1.amazonaws.com/Prod/hello/
– får jeg en ‘autentifikationsfejl.’ Det skyldes at min funktion er sat op til at bruge GET i HTTP-kaldet, så det ændrer jeg til POST i template.yaml:
Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: HelloWorldFunction Handler: v2ml.App::handleRequest Runtime: java11 MemorySize: 2048 Environment: Variables: PARAM1: VALUE Events: HelloWorld: Type: Api Properties: Path: /hello Method: post
Men det driller mig i en overraskende grad. Amazons api er nemlig ikke så godt forklaret, og det tager lidt tid at finde ud af, at når jeg sender et POST-kald, får jeg en Map som input i min funktion, og det skal vist forestille en slags interimistisk JSON-repræsentation. Her kan jeg pille kaldet ud på denne her lidt knudrede facon:
String body = (String) ((Map) input).get("body");
Det giver ikke nogle skønhedspoint, men lige nu skal jeg bare have det op og køre.
Varm ping til kold start
Det virker fint, når jeg kører min curl-kommando fra før. Men første kald tager en håndfuld sekunder. Det er det såkaldte cold start-problem, som skyldes, at runtimen lige skal komme i omdrejninger.
Der er et lille problem (nu igen,) og det består i, at hvis der går 15-20 minutter mellem kald til funktionen, pilles den ud af hukommelsen i Amazons serverless-system. Så mange kald får funktionen heller ikke i løbet af en dag, så jeg kan altså ende i en situation, hvor hvert kald tager måske 5 sekunder. Den går inte, Granberg.
Løsningen er ganske simpelt at ‘pinge’ - kalde funktionen med jævne mellemrum. Først bruger jeg bare en kommando i bash, der kører på min egen pc, men…
Helt ærligt, jeg har lavet ninja-hacks nok, siden jeg startede på det her skygge-it-projekt i… marts sidste år?
Hold da op, hvor tiden løber.
Nu må jeg dæleme bruge mere robuste løsninger. Slut med gaffa-tape, elastikker og Egon Olsen-planer.
Amazons Cloudwatch-tjeneste, der også viste Lambda-funktionens logfil tidligere, kan lige det jeg vil - pinge funktionen hver 5. minut. Jeg går ind i webkonsollen, finder Cloudwatch, og det tager ikke lang tid at sætte et ping op. Cloudwatch er også gratis at anvende, på mit niveau - indtil de gratis glæder udløber til april næste år. Den tid, den sorg.
Sidste punkt på dagsordenen er at tilrette url’en i vores webudvidelse, så den peger på vores Lambda-funktion. Jeg genindlæser webudvidelsen og smutter ind i en Version2-artikel, klikker på emneordsknappen, og så kommer de gule forslag frem i dialogen, som med sky-serveren.
Så nåede vi til vejs ende - julen er reddet på kode-redaktionen. ?
Hva' koster'et?
Men hvad koster det? Amazons gratis tilbud i serverless hedder maksimum 1 million kald om måneden og 400.000 gigabyte-sekunder. De 1 million kald er ikke noget problem - det er 32000 om dagen, og så mange artikler skriver vi heller ikke, heller ikke selvom vi pinger funktionen hver 5. minut. Men hvad er et gigabyte-sekund?
Det er mængden af RAM, vi har allokeret, ganget med den tid, et kald tager. Ifølge loggen, som vi kiggede på tidligere, tager et typisk kald efter opvarmning af runtimen 28 millisekunder. Vi allokerede 2 gigabyte i vores template.yaml-konfigurationsfil, så det giver 2 x 0,028, eller 0,056 gigabytesekunder. Og vi har altså 400.000 gratis stykker, hver måned, så længe det varer.
Når det gratis tilbud udløber, er prisen 0,20 USD pr. 1 million forespørgsler og 0,000016667 USD for hvert gigabyte-sekund. Med 2048 megabyte RAM giver det 0,000003333 USD pr. 100 millisekunder, og vores tager som sagt omkring 28 millisekunder at udføre. Så det er langt billigere, end da vi brugte en server-instans.
Efter megen bøvl endte fortællingen godt til sidst, som lovet - og mange tak til Henrik Oddershede, jeg havde ikke fundet ud af det hele uden hans hjælp.
Kode-redaktionen ønsker alle læsere en god jul og så videre. I det nye år er ambitionen at kigge på deep learning og neurale netværk til emneordene. Vi ses på den anden side.

...men det er dyrt at lave god journalistik. Derfor beder vi dig overveje at tegne abonnement på Version2.
Digitaliseringen buldrer derudaf, og it-folkene tegner fremtidens Danmark. Derfor er det vigtigere end nogensinde med et kvalificeret bud på, hvordan it bedst kan være med til at udvikle det danske samfund og erhvervsliv.
Og der har aldrig været mere akut brug for en kritisk vagthund, der råber op, når der tages forkerte it-beslutninger.
Den rolle har Version2 indtaget siden 2006 - og det bliver vi ved med.