Workshop: Google App Engine – Deel 2

Waar je vroeger een server moest kopen of huren om je nieuwe website of -applicatie te hosten, zet je die nu met een paar klikken in de cloud. Iedereen is inmiddels wel bekend met de voordelen van de cloud: goedkoop, robuust, meer flexibiliteit en makkelijker schaalbaar. In deze workshop zetten we onze eigen code in de cloud—die van Google—en bouwen we een webapplicatie met behulp van Google App Engine.

In het vorige deel van deze workshop begonnen we met het bouwen van een simpele prikbord webapplicatie. In dit deel maken we de applicatie af: we slaan de prikbord berichten op in een database, geven ze weer en laten alles er mooi uitzien met gelikte templates. De code van deze workshop is te vinden in GitHub via http://github.com/linuxmagNL, het resultaat is live te bekijken op http://linmagnl.appspot.com/.

Data opslag

De eerste taak is het opslaan van de data. Dataopslag voor een kleine applicatie is vrij simpel: er is één database op één fysieke locatie waarin je alle gegevens kunt opslaan. De cloud is natuurlijk echter vooral bedoeld voor schaalbare webapplicaties: het moet net zo goed om kunnen gaan met één gebruiker, als met honderdduizend. Dataopslag is daarmee iets ingewikkelder dan je misschien zou denken: je applicatie moet met steeds wisselende servers kunnen praten en het kan zijn dat je vorige request naar een compleet andere server gaat dan je volgende request. Die servers moeten wel steeds dezelfde data gebruiken, terwijl die data in alle waarschijnlijkheid verspreid staat over meerdere machines, waarvan sommige servers misschien wel aan de andere kant van de wereld staan. Google App Engine (GAE) maakt dit proces zo makkelijk mogelijk voor je, via de High Replication Datastore (HRD), die gebruik maakt van geavanceerde algoritmes om je data te repliceren over verschillende datacenters.

In het jargon van HRD is elk stukje data dat je naar de datastore schrijft een “entity”. Elke entity heeft een eigen unieke “key” en kan ouders (“parents”) en kinderen (“children”) hebben. Op die manier kun je je data hiërarchisch in de dataopslag kwijt. De database werkt dus anders dat je waarschijnlijk met bijvoorbeeld MySQL gewend bent: entities met een gemeenschappelijke voorouder behoren tot dezelfde groep, en de sleutel van die voorouder is de sleutel voor de gehele groep. Als je nu een zoekopdracht wil uitvoeren over bijvoorbeeld alle prikbord berichten, moet je een “ancestor query” uitvoeren op de sleutel van de gedeelde voorouder van alle berichten in je database. Dat klinkt ingewikkeld, maar in de praktijk valt het mee hoe lastig dat is, en het levert een aanzienlijk voordeel op: we hoeven nooit meer na te denken over database schaalbaarheid.

Data model

Laten we dus allereerst de sleutel aanmaken voor prikbord berichten, en een nieuwe class definiëren voor de bericht entity. Dit doen we door de volgende code in prikbord/prikbord.py te plaatsen:

from google.appengine.ext import ndb

DEFAULT_PRIKBORD_NAAM = 'default_prikbord'

def prikbord_key(prikbord_naam=DEFAULT_PRIKBORD_NAAM):
    return ndb.Key('Prikbord', prikbord_naam)

class Bericht(ndb.Model):
    gebruiker = ndb.UserProperty()
    content = ndb.StringProperty(indexed=False)
    datum = ndb.DateTimeProperty(auto_now_add=True)

Als je bekend bent met Django dan komt het weergeven en opslaan van objecten (of in MVC termen, je “Model”) op deze manier je waarschijnlijk wel bekend voor. De API voor het data model bevindt zich in de google.appengine.ext.ndb module: we genereren een sleutel voor het Bericht model en definiëren enkele eigenschappen, zoals het feit dat berichten door gebruikers worden geplaatst, dat het bericht een inhoud heeft en dat we de datum willen meegeven bij het opslaan van het bericht.

Prikbord class

Nu we ons data model duidelijk gedefinieerd hebben, kunnen we nu beginnen met het opslaan van de bericht entities in onze data store. We moeten daarvoor de post methode in de Prikbord class verder uitbreiden. In de vorige editie van de workshop deed die methode niets anders dan het weergeven van het bericht. Dit keer slaan we het bericht, samen met de gebruikersgegevens en de datum, ook echt op:

class Prikbord(webapp2.RequestHandler):

    def post(self):
        prikbord_naam = self.request.get('prikbord_naam', DEFAULT_PRIKBORD_NAAM)
        bericht = Bericht(parent=prikbord_key(prikbord_naam))
        if users.get_current_user():
            bericht.gebruiker = users.get_current_user()
        bericht.content = self.request.get('content')
        bericht.put()
        query_params = {'prikbord_naam': prikbord_naam}
        self.redirect('/?' + urllib.urlencode(query_params))

De code is vrij vanzelfsprekend: Als eerste zorgen we dat we de juiste sleutel te pakken hebben voor de prikbord_key methode. Vervolgens maken we een bericht object aan, waar we de parent key meegeven. Dit is om te zorgen dat alle berichten onder dezelfde entity groep vallen: we kunnen dus via een query met diezelfde sleutel weer heel makkelijk onze gegevens uitlezen. Daarna vullen we de gegevens in: de gebruikersnaam als die bekend is, en het bericht zelf. De datum wordt automatisch ingevuld omdat we bij dat veld auto_now_add=True hebben meegegeven in ons datamodel. Via bericht.put() slaan we het bericht op, waarna we de gebruiker weer terugverwijzen naar de index.

Uitlezen

Nu we de berichten in de database op kunnen slaan is de logische volgende stap om ze op de index pagina weer te geven. Hiervoor moeten we natuurlijk in de MainPage class zijn.

        ...
        prikbord_naam = self.request.get('prikbord_naam', DEFAULT_PRIKBORD_NAAM)
        berichten_query = Bericht.query(
            ancestor=prikbord_key(prikbord_naam)).order(-Bericht.datum)
        bericht = berichten_query.fetch(5)
        for bericht in berichten:
            if bericht.gebruiker:
                self.response.write(
                        '<b>%s</b> schreef:' % bericht.gebruiker.nickname())
            else:
                self.response.write('<b>Anoniem</b> schreef:')

            self.response.write('<blockquote>%s</blockquote>' %
                                cgi.escape(bericht.content))
        ...

Net als bij het opslaan van de berichten moeten we allereerst zorgen dat we de juiste parent key te pakken hebben. In theorie zou je ook Bericht.query() kunnen uitvoeren zonder de ancestor mee te geven, maar dan bestaat het risico dat je de meest recente berichten niet meekrijgt. Als we bijvoorbeeld zelf een bericht posten en dan naar de index worden terugverwezen, dan willen we natuurlijk wel ons eigen bericht zien. Database consistentie is alleen gegarandeerd als we de ancestor specificeren. Je kunt trouwens ook GQL gebruiken om met de database te praten, een query taal die erg op de standaard SQL lijkt.

We sorteren de berichten op de datum van plaatsing (meest recente bovenaan) en pakken de eerste vijf om weer te geven. Via een simpele loop gaan we één voor één de berichten af, en geven we de inhoud netjes via cgi.escape() weer om te zorgen dat hackers geen cross-site scripting (XSS) aanvallen op onze applicatie kunnen uitvoeren, door bijvoorbeeld een <script> te injecteren.

Index

Net als bij een relationele database moeten we, om te zorgen dat we zo snel mogelijk queries kunnen uitvoeren, aangeven welke zoek indices we voor onze database configuratie willen hebben. Omdat we in de MainPage class de berichten sorteren op datum, is het van belang dat we aan de datastore vertellen dat we een index op het datum veld willen hebben. Hiervoor moeten we een apart bestand toevoegen, index.yaml, waarin de indices van onze datastore kunnen worden opgeslagen. Dat bestand ziet er in ons geval als volgt uit:

indexes:
- kind: Bericht
  ancestor: yes
  properties:
  - name: datum
    direction: desc

Templates

We hebben de backend nu helemaal werkend. Omdat de code helemaal is ingesteld op schaalbaarheid hoeven we er nooit meer naar om te kijken, ook niet als onze webapp enorm populair wordt. De volgende stap is het uiterlijk van onze app: in de vorige editie definiëerden we een MAIN_PAGE_HTML variabele waarin we enkele gegevens invulden. Dat was prima voor een Hello World voorbeeld, maar op ten duur is zoiets voor een serieuze webapplicatie natuurlijk niet in stand te houden. De oplossing is om de HTML code uit te splitsen in aparte template bestanden.

Er bestaan een paar hele goede template systemen voor Python. De bekendste is waarschijnlijk het systeem van Django. Google App Engine ondersteunt standaard Django en Jinja2, maar het is niet erg moeilijk om een ander systeem samen met je webapplicatie te bundelen. In deze workshop kiezen we voor Jinja2, dat oorspronkelijk is gebouwd op Django maar dat algemeen wordt beschouwd als sneller en meer flexibel.

Jinja2

Om van Jinja2 gebruik te kunnen maken moeten we allereerst de library inladen. Om dat aan App Engine te vertellen voegen we het volgende aan app.yaml toe:

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

Voor een serieuze webapplicatie is het natuurlijk geen goed idee om als versie “latest” te gebruiken, maar voor nu is dat ok. Vervolgens initialiseren we in prikbord.py de Jinja2 omgeving, door aan het begin toe te voegen:

import jinja2

JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

MainPage

We verwijderen de MAIN_PAGE_HTML variabele uit prikbord.py, want de HTML code brengen we als gezegd nu onder in een apart template bestand. We moeten echter wel zorgen dat we de juiste gegevens in die HTML kunnen weergeven. Om dat te doen plaatsen we alles in een standaard Python dictionary en geven we dat via template.render() aan de Jinja2 omgeving om af te handelen:

     
      template_values = {
            'berichten': berichten,
            'prikbord_naam': urllib.quote_plus(prikbord_naam)
            … # andere waarden
      }

      template = JINJA_ENVIRONMENT.get_template('index.html')
      self.response.write(template.render(template_values))
      …

Index.html

De laatste stap is om nu een apart HTML bestand aan te maken om de gegevens weer te geven:

<!DOCTYPE html>
{% autoescape true %}
<html>
  <body>
    {% for bericht in berichten %}
      {% if bericht.gebruiker %}
        <b>{{ bericht.gebruiker.nickname() }}</b> schreef:
      {% else %}
       Anoniem schreef:
      {% endif %}
      <blockquote>{{ bericht.content }}</blockquote>
    {% endfor %}
    <form action="/prik?prikbord_naam={{ prikbord_naam }}" method="post">
      <div><textarea name="content" rows="3" cols="60" placeholder="Je bericht..."></textarea></div>
      <div><input type="submit" value="Prik!"></div>
    </form>
    <hr>
    <a href="/{{ url|safe }}">{{ url_linktext }}</a>
  </body>
</html>
{% endautoescape %}

Je herkent de syntax waarschijnlijk van andere template systemen: tussen {% en %} tekens kunnen we in een soort pseudoprogrammeertaal als-dan-constructies en loops implementeren.

Styles

Elke webapplicatie in App Engine werkt met dynamisch gegenereerde HTML vanuit de code, rechtstreeks of via templates. Daarnaast moet een applicatie echter ook statische content kunnen weergeven: denk hierbij aan plaatjes, filmpjes, Flash, CSS en javascript bestanden. Om zo efficient mogelijk te zijn behandelt App Engine dat soort bestanden anders. Je kunt via app.yaml aangeven dat statische bestanden op een andere manier moeten worden behandeld door aan te geven dat een bepaalde map statische bestanden bevat. Je kunt bijvoorbeeld het volgende toevoegen:

handlers:
- url: /stylesheets
  static_dir: stylesheets
- url: /.*
 
script: prikbord.application

App Engine weet nu dat het stylesheets/ als een statische map moet behandelen. De rest van alle requests gaat naar onze prikbord applicatie en wordt daar afgehandeld. In feite maakt je dus een aparte handler aan voor je stylesheets. Maak een stylesheets map aan en plaats daarin een bestand style.css met je eigen CSS code. Je kunt nu in je template bestand een CSS bestand uitlezen:
</head>

    <link type="text/css" rel="stylesheet" href="/stylesheets/style.css" />
</head>

Deployen

Dat zijn een hoop veranderingen! De backend en frontend zijn compleet omgegooid en functioneren nu zoals ze in een volwaardige webapplicatie zouden doen. Net als de vorige keer kunnen we de code online zetten door het appcfg.py commando aan te roepen:

$ google_appengine/appcfg.py update linmagnl

Als alles goed is gegaan kun je nu je eigen applicatie bekijken op jouwnaam.appspot.com!

Conclusie

En daarmee is deze workshop op haar einde gekomen. De code van deze workshop is beschikbaar op http://www.github.com/linuxmagNL.

Google App Engine is een goed voorbeeld van een Platform-as-a-Service in de cloud, waarbij we via een API in een handomdraai een volwaardige webapplicatie kunnen opzetten. Speerpunten van GAE zijn het gebruikersgemak en de automatische schaalbaarheid. Zoals we konden zien bij het programmeren van de database interactie, hoeven we nooit meer om te kijken naar die code, zelfs niet als onze web applicatie enorm populair wordt en honderd duizenden database interacties moet uitvoeren.

We hebben hier slechts het tipje van de sluier opgelicht: van “Hallo Wereld” naar een simpele Prikbord applicatie. Daarmee hebben we wel bijna alle aspecten van een web applicatie behandeld. De volgende stap is om te kijken naar de vele modules die GAE je biedt, van Memcache om je applicatie nog sneller te maken tot geavanceerde afhandeling van plaatjes en zoekopdrachten. Veel succes en plezier met het bouwen van je eigen webapp!

Referenties

https://developers.google.com/appengine/

NEDLINUX FORUM

Het nederlandse linuxforum
Voor beginners en pro’s

 

 

 

 

E-mailadres



 

 

Nieuwste editie:

Linuxmag op Facebook

@linuxmagnl op Twitter

linuxmagNL Linux Nieuws: @SUSE bestaat 25 jaar en trakteert! Maak kans op entreeticket voor #SUSECON in Praag, zie link!… https://t.co/ENJKDvyZQ8
linuxmagNL De nieuwe editie van Linux Magazine is weer uit! Thema: bescherm jezelf tegen hackers met Linux. Veel leesplezier a… https://t.co/Zcy3Zdjb90
linuxmagNL Ook de Red Hat Forum BeNeLux 2017 mag je dit jaar niet missen. 10 oktober 2017, zet het in je agenda! https://t.co/niY9UdK3Ov