Google Cloud Run ja Firestore

Tässä pääteohjauksessa siirretään Flask-sovellus Googlen Cloud Run -palveluun, lisätään siihen Google-kirjautuminen ja tallennetaan tiedot Firestore-tietokantaan. Taustaksi kannattaa lukea luento Google Cloud Run, Datastore ja Firestore, jossa käsitellään kontit, Gunicorn, Firestoren tietomalli ja kustannukset.

Pääteohjauksen vanha versio

Komennot on kirjoitettu Linuxin ja macOS:n komentotulkille (bash). Windowsissa kaikki tehdään WSL:ssä eli Windowsin Linux-alijärjestelmässä, jossa komennot toimivat sellaisinaan.

Valmistelut

Opetuskäytön kuponki

Ota ensimmäisenä käyttöön Googlen opetuskäytön kuponki. Sen avulla saat laskutustilin antamatta omia luottokorttitietojasi. Cloud Run ja Firestore vaativat laskutustilin, vaikka tämän ohjauksen käyttö pysyy ilmaiskiintiöiden sisällä.

Windows: WSL

Windowsissa suositeltu tapa on WSL (Windows Subsystem for Linux), joka ajaa Linuxia Windowsin rinnalla. Siinä toimivat kaikki tämän ohjeen komennot sellaisinaan, samoin Gunicorn, joka ei toimi Windowsissa. Myös Cloud Runin kontti on Linux, joten WSL on lähempänä tuotantoympäristöä kuin Windows.

Asenna WSL avaamalla PowerShell järjestelmänvalvojana ja antamalla komento:

wsl --install

Komento asentaa oletuksena Ubuntun. Käynnistä kone uudelleen, avaa Ubuntu Käynnistä-valikosta ja luo sille käyttäjätunnus ja salasana. Tästä eteenpäin kaikki komennot annetaan Ubuntun pääteikkunassa. Asenna ensin Pythonin virtuaaliympäristöt ja Java, jota tietokannan emulaattori tarvitsee:

sudo apt-get update
sudo apt-get install -y python3-venv python3-pip default-jre-headless
Pidä projekti WSL:n puolella

Kun sovellus ajetaan WSL:ssä, sitä käytetään Windowsin selaimella tavalliseen tapaan osoitteessa http://localhost:8080, koska WSL välittää Windowsin localhost-osoitteen WSL:n palveluille.

Google Cloud CLI

Ubuntussa ja WSL:ssä Google Cloud CLI asennetaan Googlen pakettivarastosta:

sudo apt-get install -y apt-transport-https ca-certificates gnupg curl
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg \
    | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
    | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
sudo apt-get update
sudo apt-get install -y google-cloud-cli google-cloud-cli-firestore-emulator

Samalla asentuu myös Firestore-emulaattori, jota käytetään myöhemmin. macOS:ssä ja muissa Linux-jakeluissa noudata Googlen asennusohjetta. Kirjaudu sen jälkeen sisään:

gcloud init
gcloud auth login

Kirjautuminen avaa selaimen. Jos selain ei WSL:ssä aukea itsestään, gcloud tulostaa osoitteen, jonka voit kopioida Windowsin selaimeen. Saman voi pyytää suoraan valitsimella --no-launch-browser.

Projekti

Projektin tunnisteen on oltava yksilöllinen kaikkien Google Cloudin käyttäjien kesken. Lisää siihen siksi jokin oma tunniste, esimerkiksi käyttäjätunnuksesi. Tässä ohjeessa projektin nimi ja alue tallennetaan ympäristömuuttujiin, joten komentoja ei tarvitse muokata:

export PROJEKTI=ties4080-ohjaus4-omatunnus
export ALUE=europe-north1

gcloud projects create $PROJEKTI
gcloud config set project $PROJEKTI

Ympäristömuuttujat ovat voimassa vain siinä pääteikkunassa, jossa ne on asetettu. Jos avaat uuden ikkunan, anna export-komennot uudelleen, tai lisää ne tiedoston ~/.bashrc loppuun, jolloin ne asetetaan automaattisesti.

Liitä projektiin kupongin laskutustili. Tilin tunnisteen näet ensimmäisellä komennolla:

gcloud billing accounts list
gcloud billing projects link $PROJEKTI --billing-account=TILIN_TUNNISTE

Ota käyttöön tarvittavat palvelut:

gcloud services enable run.googleapis.com cloudbuild.googleapis.com \
    artifactregistry.googleapis.com firestore.googleapis.com \
    secretmanager.googleapis.com

Palvelutilin oikeudet

Cloud Run käyttää sekä kontin rakentamiseen että sovelluksen ajamiseen projektin Compute Engine -oletuspalvelutiliä. Sen osoite muodostuu projektin numerosta, joka tallennetaan tässä muuttujaan. Palvelutili tarvitsee oikeuden rakentaa kontteja ja käyttää Firestorea:

export NUMERO=$(gcloud projects describe $PROJEKTI --format="value(projectNumber)")
export PALVELUTILI=$NUMERO-compute@developer.gserviceaccount.com

gcloud projects add-iam-policy-binding $PROJEKTI \
    --member=serviceAccount:$PALVELUTILI --role=roles/run.builder

gcloud projects add-iam-policy-binding $PROJEKTI \
    --member=serviceAccount:$PALVELUTILI --role=roles/datastore.user

Rooli roles/datastore.user antaa luku- ja kirjoitusoikeuden Firestoreen; nimi on peräisin ajalta, jolloin Firestore oli vielä Datastore. Oikeuksien voimaantulo voi kestää muutaman minuutin.

Aseta budjettihälytys

Aseta Google Cloud -konsolin Budgets & alerts -sivulla projektille pieni budjetti, esimerkiksi 5 euroa, ja hälytys. Budjetti ei pysäytä palveluja, mutta saat ilmoituksen ajoissa, jos kulut alkavat kasvaa.

Hello world Cloud Runissa

Tee uusi kansio ja siihen kolme tiedostoa kuten luennossa:

# main.py
import os

from flask import Flask

app = Flask(__name__)


@app.route("/")
def etusivu():
    return "Hei Cloud Runista!"


if __name__ == "__main__":
    app.run(debug=True, port=int(os.environ.get("PORT", 8080)))
# requirements.txt
Flask
gunicorn
# Procfile
web: gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

Lisää kansioon myös .gcloudignore-tiedosto. Se kertoo, mitä ei lähetetä pilveen julkaisun yhteydessä. Ilman sitä julkaisu lähettää koko kansion sisältöineen, myös virtuaaliympäristön ja mahdolliset salaisuudet:

# .gcloudignore
.gcloudignore
.git
.gitignore
.venv/
venv/
__pycache__/
.env

Tee projektikansioon virtuaaliympäristö ja asenna riippuvuudet siihen. Ubuntu ei salli kirjastojen asentamista pipillä järjestelmän Pythoniin, joten virtuaaliympäristö on WSL:ssä välttämätön:

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Virtuaaliympäristö on otettava käyttöön source-komennolla aina, kun avaat uuden pääteikkunan. Kokeile sovellusta ensin omalla koneella Flaskin kehityspalvelimella (python main.py) ja julkaise se sitten:

gcloud run deploy ohjaus4 --source . \
    --region $ALUE \
    --allow-unauthenticated \
    --max-instances 2 \
    --memory 256Mi \
    --execution-environment gen1

--memory 256Mi puolittaa instanssin muistin oletusarvosta 512 MiB, mikä riittää pienelle sovellukselle ja alentaa kustannuksia. Alle 512 MiB:n muisti edellyttää ensimmäisen sukupolven suoritusympäristöä (--execution-environment gen1).

Ensimmäinen julkaisu kestää muutaman minuutin, koska Cloud Build rakentaa kontin. Lopuksi komento tulostaa osoitteen, joka on muotoa https://ohjaus4-…​.europe-north1.run.app. Julkaistu palvelu löytyy myös Cloud Run -konsolista, josta näet esimerkiksi lokit ja revisiot.

Lisätietoa: Quickstart: Deploy a Python (Flask) web app to Cloud Run ja Deploy services from source code.

Kirjautuminen Google-tunnuksella

Google-kirjautuminen perustuu OAuth 2.0- ja OpenID Connect -protokolliin. Sovellus ohjaa käyttäjän Googlen kirjautumissivulle, ja kirjautumisen jälkeen Google ohjaa käyttäjän takaisin sovelluksen määrättyyn osoitteeseen mukanaan tieto käyttäjästä. Flaskissa tähän käytetään Authlib-kirjastoa.

Asetukset Google Auth Platformissa

Tee seuraavat asetukset Google Cloud -konsolin Google Auth Platform -osiossa:

  1. Branding: anna sovellukselle nimi, esimerkiksi TIES4080 ohjaus4, ja tukisähköpostiosoite. Vain pakolliset kentät tarvitsee täyttää.
  2. Audience: valitse käyttäjätyypiksi External. Internal toimisi vain saman organisaation tunnuksilla.
  3. Data Access: lisää oikeudet openid, …/auth/userinfo.email ja …/auth/userinfo.profile. Muita oikeuksia ei tarvita. Nämä kolme kuuluvat Googlen ei-arkaluonteisiin oikeuksiin, joten sovellus ei tarvitse Googlen tarkastusta.
  4. Audience → Publish app: julkaise sovellus, jolloin sen tilaksi tulee In production. Uusi sovellus on aluksi testaustilassa, jossa kirjautuminen on tarkoitettu vain erikseen lisätyille testikäyttäjille. Viikkotehtävän sovellukseen kirjautuvat myös ne, jotka testaavat ja arvioivat sitä, eikä heidän tunnuksiaan tiedetä etukäteen, joten sovellus on julkaistava. Tarkastamattoman sovelluksen kirjautumisikkunassa ei välttämättä näy sovelluksen nimeä, mutta kirjautuminen toimii kaikilla Google-tunnuksilla.
  5. Clients → Create client: valitse tyypiksi Web application ja täytä osoitteet:
    • Authorized JavaScript origins: http://localhost:8080 ja myöhemmin Cloud Run -palvelusi osoite, esimerkiksi https://ohjaus4-…​.europe-north1.run.app. Alkuperä ei saa sisältää polkua eikä loppukauttaviivaa.
    • Authorized redirect URIs: http://localhost:8080/auth, http://127.0.0.1:8080/auth ja Cloud Run -palvelun osoite /auth-polulla. Tähän osoitteeseen Google ohjaa käyttäjän kirjautumisen jälkeen, ja sen on vastattava sovelluksen osoitetta täsmälleen: protokolla, palvelin, portti ja polku.

Lopuksi saat Client ID- ja Client secret -tunnisteet. Kopioi ne talteen; salaisuutta ei näytetä uudelleen.

Sovellus

Lisää requirements.txt-tiedostoon rivit Authlib ja requests. Sovellus lukee tunnisteet ympäristömuuttujista; Authlib poimii ne itse asetuksista GOOGLE_CLIENT_ID ja GOOGLE_CLIENT_SECRET:

import os

from authlib.integrations.flask_client import OAuth
from flask import Flask, Response, redirect, render_template, session, url_for
from werkzeug.middleware.proxy_fix import ProxyFix


class XhtmlResponse(Response):
    default_mimetype = "application/xhtml+xml"


app = Flask(__name__)
app.response_class = XhtmlResponse
app.secret_key = os.environ["SECRET_KEY"]
app.config["GOOGLE_CLIENT_ID"] = os.environ["GOOGLE_CLIENT_ID"]
app.config["GOOGLE_CLIENT_SECRET"] = os.environ["GOOGLE_CLIENT_SECRET"]

# Cloud Runissa sovelluksen edessä on Googlen välityspalvelin, joka purkaa
# HTTPS-salauksen ja välittää pyynnön konttiin tavallisena HTTP:nä. ProxyFix
# lukee alkuperäisen protokollan ja osoitteen välityspalvelimen otsakkeista,
# jotta url_for(..., _external=True) tuottaa oikean https-osoitteen.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)

oauth = OAuth(app)
oauth.register(
    name="google",
    server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
    client_kwargs={"scope": "openid email profile"},
)


@app.route("/")
def etusivu():
    return render_template("etusivu.xhtml", kayttaja=session.get("kayttaja"))


@app.route("/login")
def login():
    paluuosoite = url_for("auth", _external=True)
    # select_account näyttää tilinvalinnan, vaikka käyttäjä olisi jo
    # kirjautunut Googleen
    return oauth.google.authorize_redirect(paluuosoite, prompt="select_account")


@app.route("/auth")
def auth():
    token = oauth.google.authorize_access_token()
    session.clear()
    session["kayttaja"] = token["userinfo"]
    return redirect(url_for("etusivu"))


@app.post("/logout")
def logout():
    session.clear()
    return redirect(url_for("etusivu"))

session["kayttaja"] on sanakirja, jossa on tietoja kirjautuneesta käyttäjästä. Esimerkiksi sähköpostiosoite on avaimella email ja nimi avaimella name.

Valmis esimerkki löytyy Authlibin demosovelluksesta ja kurssin mallikoodista google-login-cloud-run.zip.

Miksi uloskirjautuminen ei näytä toimivan?

Uloskirjautuminen tyhjentää vain sovelluksen oman session. Käyttäjä on edelleen kirjautuneena Googleen, ja jos hän painaa uudelleen kirjautumislinkkiä, Google palauttaa hänet sovellukseen kysymättä salasanaa. Näin kuuluu toimia: sovellus ei saa kirjata käyttäjää ulos Googlesta. Yllä käytetty prompt="select_account" näyttää silti tilinvalinnan, jolloin käyttäjä näkee, millä tilillä hän kirjautuu. Kts. Prompting re-consent.

Salaisuudet Cloud Runiin

Omalla koneella tunnisteet annetaan ympäristömuuttujina ennen sovelluksen käynnistämistä. Cloud Runissa salaisuudet tallennetaan Secret Manageriin. Luo salaisuudet kerran ja anna palvelutilille oikeus lukea niitä:

python3 -c "import secrets; print(secrets.token_hex(32), end='')" \
    | gcloud secrets create flask-avain --data-file=-
printf '%s' 'TÄHÄN_CLIENT_SECRET' \
    | gcloud secrets create google-client-secret --data-file=-

for salaisuus in flask-avain google-client-secret; do
    gcloud secrets add-iam-policy-binding $salaisuus \
        --member=serviceAccount:$PALVELUTILI \
        --role=roles/secretmanager.secretAccessor
done

Salaisuudet voi luoda myös konsolin Secret Manager -sivulla. Client ID ei ole salainen, joten se annetaan tavallisena ympäristömuuttujana:

gcloud run deploy ohjaus4 --source . \
    --region $ALUE \
    --allow-unauthenticated \
    --max-instances 2 \
    --memory 256Mi \
    --execution-environment gen1 \
    --set-env-vars GOOGLE_CLIENT_ID=TÄHÄN_CLIENT_ID \
    --set-secrets SECRET_KEY=flask-avain:latest,GOOGLE_CLIENT_SECRET=google-client-secret:latest

Lisää julkaisun jälkeen Cloud Run -palvelun osoite Google Auth Platformin asetuksiin kuten yllä.

Firestore

Firestore-tietokanta luodaan suoraan Google Cloud -konsolissa tai komentoriviltä. Firebase-projektia ei tarvita, koska tietokantaa käytetään vain palvelimelta.

Tietokannan luominen

Komentoriviltä:

gcloud firestore databases create --location=$ALUE

Komento luo tietokannan tunnisteella (default), Native-tilassa ja Standard-versiona, koska ne ovat oletusarvot.

Konsolissa: avaa Firestore ja valitse Create database. Valitse:

Luo projektiin vain yksi tietokanta. Ilmaiskiintiö koskee vain yhtä tietokantaa projektia kohden, ja sen saa ensimmäinen luotu tietokanta.

Tunnistautuminen omalla koneella

Cloud Runissa sovellus tunnistautuu automaattisesti palvelutilin avulla. Omalla koneella kirjaudutaan kerran omalla Google-tunnuksella, jonka jälkeen Googlen kirjastot löytävät tunnukset itse:

gcloud auth application-default login
gcloud auth application-default set-quota-project $PROJEKTI
Älä luo palvelutilin avaintiedostoa

Vanhoissa ohjeissa neuvotaan usein luomaan palvelutilille JSON-avain ja tallentamaan se projektikansioon. Älä tee niin. Avaintiedosto on pysyvä tunnus, joka päätyy helposti versionhallintaan tai julkaistavaan konttiin, ja Google estää avainten luomisen monessa organisaatiossa oletuksena. Yllä oleva kirjautuminen korvaa avaimen kokonaan.

Emulaattori

Kehitystyö kannattaa tehdä emulaattorilla: se ei kuluta ilmaiskiintiötä, ja tietokannan voi sotkea ja tyhjentää huoletta. WSL:ssä ja Ubuntussa emulaattori ja sen tarvitsema Java asennettiin jo valmisteluissa. Jos asensit Google Cloud CLI:n Googlen asennusohjelmalla, asenna emulaattori komennolla gcloud components install cloud-firestore-emulator ja lisäksi jokin Javan ajoympäristö.

Käynnistä emulaattori omaan pääteikkunaansa. Portti annetaan erikseen, koska emulaattorin oletusportti 8080 on sama kuin sovelluksen:

gcloud emulators firestore start --host-port=localhost:8081

Kerro sovellukselle emulaattorin osoite ennen sen käynnistämistä. Kun FIRESTORE_EMULATOR_HOST on asetettu, kirjasto ottaa yhteyden emulaattoriin eikä pilvessä olevaan tietokantaan:

export FIRESTORE_EMULATOR_HOST=localhost:8081
export GOOGLE_CLOUD_PROJECT=$PROJEKTI

Emulaattori säilyttää tiedot vain muistissa, joten ne katoavat, kun emulaattori suljetaan. Kun haluat käyttää pilvessä olevaa tietokantaa, poista muuttuja (unset FIRESTORE_EMULATOR_HOST).

Ensimmäinen kokeilu

Lisää requirements.txt-tiedostoon rivi google-cloud-firestore ja asenna se. Tutustu dokumenttiin Firestore data model ja kokeile seuraavaa ohjelmaa:

# kokeilu.py
from google.cloud import firestore

db = firestore.Client()

db.collection("kayttajat").document("alovelace").set({
    "etunimi": "Ada",
    "sukunimi": "Lovelace",
    "syntynyt": 1815,
})
db.collection("kayttajat").document("aturing").set({
    "etunimi": "Alan",
    "toinen_nimi": "Mathison",
    "sukunimi": "Turing",
    "syntynyt": 1912,
})

for dokumentti in db.collection("kayttajat").stream():
    print(f"{dokumentti.id} => {dokumentti.to_dict()}")

Tässä dokumenttien tunnisteet on annettu itse, mikä sopii kokeiluun. Kuten luennossa todetaan, oikeassa sovelluksessa kannattaa yleensä käyttää Firestoren itse luomia tunnisteita.

Tutki tietokannan sisältöä pilvessä Firestore-konsolissa. Tutustu sitten Firestoren perusoperaatioihin:

Oppaiden esimerkeissä voi valita ohjelmointikielen; valitse Python.

Reseptitietokanta

Rakennetaan reseptitietokanta, jossa jokainen resepti kuuluu johonkin ruokalajiin. Reseptit tallennetaan ruokalajin alikokoelmaan ruokalajit/{ruokalaji}/reseptit, koska sovellus hakee reseptit lähes aina ruokalajeittain.

Alikokoelmaa ei tarvita transaktioiden takia. Firestoren transaktio voi koskea mitä tahansa dokumentteja riippumatta siitä, missä kokoelmissa ne ovat. Hierarkiaa käytetään tiedon järjestämiseen ja kyselyjen rajaamiseen.

Tietojen lisääminen

Ruokalajit ja reseptit saavat Firestoren luomat tunnisteet. Koska tunnisteet ovat satunnaisia, ruokalajien järjestys tallennetaan omaan kenttäänsä:

from google.cloud import firestore

db = firestore.Client()

ruokalajit = [
    {"nimi": "Alkuruoka", "reseptit": []},
    {"nimi": "Pääruoka", "reseptit": [
        {"nimi": "Ananas Con Carne", "kuvaus": "Tulinen", "henkilomaara": 4},
        {"nimi": "Banaanicurry", "kuvaus": "Mainio kasvisruoka", "henkilomaara": 2},
    ]},
    {"nimi": "Lisäkeruoka", "reseptit": []},
    {"nimi": "Väli- tai iltapala", "reseptit": []},
    {"nimi": "Jälkiruoka", "reseptit": []},
    {"nimi": "Suolainen leivonnainen", "reseptit": []},
    {"nimi": "Makea leivonnainen", "reseptit": [
        {"nimi": "Tiramisu", "kuvaus": "Hieno herkku", "henkilomaara": 6},
        {"nimi": "Porkkanapiirakka", "kuvaus": "Suuri herkku", "henkilomaara": 8},
        {"nimi": "Rommikakku", "kuvaus": "Hyytelökakku, ei tarvi paistaa", "henkilomaara": 6},
        {"nimi": "Kauralastut", "kuvaus": "Helppo ja nopea", "henkilomaara": 4},
    ]},
]

# Kaikki lisäykset tehdään yhtenä transaktiona: joko kaikki tallentuvat
# tai ei mikään.
transaktio = db.batch()
for jarjestys, ruokalaji in enumerate(ruokalajit, start=1):
    laji = db.collection("ruokalajit").document()          # uusi tunniste
    transaktio.set(laji, {"nimi": ruokalaji["nimi"], "jarjestys": jarjestys})
    for resepti in ruokalaji["reseptit"]:
        transaktio.set(laji.collection("reseptit").document(), resepti)
transaktio.commit()

Firestoressa transaktio, joka vain kirjoittaa, tehdään batch()-oliolla. Jos transaktion täytyy myös lukea tietoja ja tehdä kirjoitukset niiden perusteella — esimerkiksi kasvattaa laskuria — käytetään transaction()-metodia. Kts. Transactions and batched writes.

document() ilman argumenttia luo viittauksen uuteen dokumenttiin, jolla on satunnainen tunniste. Tunnisteen saa ominaisuudesta id jo ennen tallentamista. Katso Firestore-konsolista, miltä hierarkia näyttää, ja aja ohjelma uudelleen: mitä tapahtuu?

Tietojen hakeminen

Yhden ruokalajin reseptit haetaan sen alikokoelmasta:

reseptit = db.collection("ruokalajit", ruokalaji_id, "reseptit").stream()
for resepti in reseptit:
    print(f"{resepti.id} => {resepti.to_dict()}")

Yritä listata reseptit ruokalajeittain seuraavaan tapaan:

Alkuruoka
Pääruoka
        Ananas Con Carne
        Banaanicurry
Lisäkeruoka
Väli- tai iltapala
Jälkiruoka
Suolainen leivonnainen
Makea leivonnainen
        Tiramisu
        Porkkanapiirakka
        Rommikakku
        Kauralastut

Malli:

for ruokalaji in db.collection("ruokalajit").order_by("jarjestys").stream():
    print(ruokalaji.get("nimi"))
    for resepti in ruokalaji.reference.collection("reseptit").stream():
        print(f"\t{resepti.get('nimi')}")

Laske, montako dokumenttia listaus lukee: jokainen ruokalaji ja jokainen resepti on yksi luku. Kuten luennon esimerkkilaskelmassa, sivun rakenne vaikuttaa suoraan kustannuksiin.

Kaikki reseptit riippumatta ruokalajista haetaan collection group -kyselyllä, joka kohdistuu kaikkiin samannimisiin alikokoelmiin:

for resepti in db.collection_group("reseptit").stream():
    print(f"{resepti.reference.parent.parent.id} / {resepti.id} => {resepti.to_dict()}")

resepti.reference.parent on reseptin kokoelma ja parent.parent ruokalajin dokumentti, johon resepti kuuluu.

Jos kyselyyn lisää ehdon tai järjestyksen, esimerkiksi .order_by("nimi"), Firestore saattaa vaatia uuden indeksin. Ohjelma kaatuu silloin virheeseen, jonka viestissä on suora linkki indeksin luomiseen. Indeksin rakentaminen kestää hetken, ja sillä välin kysely antaa edelleen virheen.

Tehtävät

  1. Lisää Flask-sovellukseen sivu, joka listaa reseptit ruokalajeittain yllä olevan mallin mukaan.
  2. Listaa reseptien yhteydessä myös niiden henkilömäärät.
  3. Tee WTForms-lomake ruokalajien lisäämiseen. Kun lisääminen onnistuu, muuta lomake muokkauslomakkeeksi ja tee ruokalajilistauksesta linkkejä, joilla muokattava ruokalaji valitaan.
  4. Tee vastaava lomake resepteille. Käytä muotoa /reseptit/ruokalajin-id/reseptin-id olevia osoitteita:

    @app.route("/reseptit/<ruokalaji_id>/<resepti_id>", methods=["GET", "POST"])
    def resepti(ruokalaji_id, resepti_id):
        viittaus = db.collection("ruokalajit", ruokalaji_id, "reseptit").document(resepti_id)
        dokumentti = viittaus.get()
        if not dokumentti.exists:
            abort(404)
        lomake = ReseptiLomake(data=dokumentti.to_dict())
        ...

    Firestoren tunnisteet ovat merkkijonoja, joten niitä ei muunneta kokonaisluvuiksi.

  5. Kun tallennus onnistuu, ohjaa käyttäjä edelleen listaussivulle (POST/Redirect/GET). Jos lomakkeessa on virheitä, näytä lomake uudelleen virheilmoituksineen.

Välimuisti: Flask-Caching

Jokainen Firestoresta luettu dokumentti maksaa, ja moni tieto muuttuu harvoin. Flask-Caching-kirjastolla tulokset voi säilyttää hetken muistissa, jolloin samaa tietoa ei haeta joka pyynnöllä uudelleen.

pip install Flask-Caching
from flask_caching import Cache

app.config.from_mapping({
    "CACHE_TYPE": "SimpleCache",       # välimuisti instanssin muistissa
    "CACHE_DEFAULT_TIMEOUT": 60,       # oletuselinikä sekunteina
})
cache = Cache(app)

Funktion tuloksen tallentaminen

Hyödyllisintä on tallentaa välimuistiin tietokantahakujen tulokset. memoize tallentaa tuloksen funktion argumenttien mukaan:

@cache.memoize(timeout=300)
def ruokalajit():
    return [(d.id, d.to_dict())
            for d in db.collection("ruokalajit").order_by("jarjestys").stream()]


def lisaa_ruokalaji(tiedot):
    db.collection("ruokalajit").add(tiedot)
    # muutoksen jälkeen vanha tulos poistetaan välimuistista
    cache.delete_memoized(ruokalajit)

Kokeile kutsua funktiota useita kertoja ja seuraa Firestore-konsolin käyttötilastoista, kasvaako lukujen määrä.

Koko sivun tallentaminen

cached-dekoraattori tallentaa koko sivun. Sitä käytetään route-dekoraattorin jälkeen:

@app.route("/reseptit")
@cache.cached(timeout=60)
def reseptilistaus():
    ...
Älä tallenna käyttäjäkohtaisia sivuja

@cache.cached käyttää avaimena oletuksena pelkkää sivun polkua. Jos sivun sisältö riippuu kirjautuneesta käyttäjästä, ensimmäisen käyttäjän sivu näytetään kaikille seuraaville — myös hänen henkilökohtaiset tietonsa. Tallenna välimuistiin vain sisältöä, joka on sama kaikille, tai käytä memoizea funktioille, joiden argumenteissa käyttäjä on mukana.

Välimuisti Cloud Runissa

SimpleCache on käytännössä sanakirja instanssin muistissa. Jokaisella Cloud Run -instanssilla on siis oma välimuistinsa, ja se tyhjenee, kun instanssi sammutetaan. Jos instansseja on useita, eri pyynnöt voivat saada eri-ikäistä tietoa, kunnes välimuistin elinikä umpeutuu. Välimuisti kuluttaa myös instanssin muistia. Yhteisen välimuistin saisi Googlen maksullisella Memorystore-palvelulla, mutta sitä ei tällä kurssilla käytetä.

Mieti siksi tarkkaan, mitä tietoa voi näyttää vähän vanhentuneena. Reseptitietokannassa välimuistia voi käyttää paljon ja pitkiä aikoja, koska reseptit muuttuvat harvoin. Tulospalvelussa kilpailut ja sarjat muuttuvat harvoin, mutta joukkueiden ja leimausten tiedot usein; tuloslistauksessa 30 sekunnin viive olisi silti vielä hyväksyttävä.

Cloud Runin asetukset ja kustannukset

Asetuksia muutetaan komentorivin valitsimilla julkaisun yhteydessä tai konsolissa kohdasta Edit & deploy new revision. Jokainen muutos luo sovelluksesta uuden revision, joka ottaa oletuksena kaiken liikenteen. Vanha revisio kannattaa säilyttää hetken, jotta liikenteen voi siirtää takaisin, jos uudessa on virhe. Kts. Configure Cloud Run services.

Kts. myös General development tips.

Firestoren kustannukset

Firestoressa kustannus syntyy luettujen, kirjoitettujen ja poistettujen dokumenttien määrästä, ei niinkään tallennetun tiedon määrästä. Ilmaiskiintiö on päivittäinen ja koskee yhtä tietokantaa projektia kohden; sillä pystyy tekemään jo isohkojakin sovelluksia maksamatta mitään. Luennon esimerkkilaskelma näyttää, miten tietokannan ja sivujen rakenne vaikuttaa lukujen määrään. Kts. Firestore pricing.

Siivous

Jokainen julkaisu tallentaa uuden levykuvan Artifact Registryyn. Tallennustila maksaa ilmaiskiintiön ylittävältä osalta, joten poista vanhat levykuvat ajoittain Artifact Registry -konsolista. Kun et enää tarvitse palvelua, poista se:

gcloud run services delete ohjaus4 --region $ALUE

Lisätietoa

Käyttäjien kommentit

Kommentoi Lisää kommentti