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.
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
- Tee projektikansiot WSL:n kotihakemistoon, esimerkiksi
~/ties4080/ohjaus4, ei Windowsin puolelle polkuun/mnt/c/…. Windowsin tiedostojärjestelmän käyttö WSL:stä on hidasta, eikä Flask välttämättä huomaa tiedostojen muutoksia. - Muokkaa tiedostoja esimerkiksi VS Codella, johon on asennettu
WSL-laajennus. Kun
kirjoitat Ubuntun pääteikkunassa projektikansiossa
code ., VS Code avaa kansion suoraan WSL:stä. - Asenna Google Cloud CLI vain WSL:ään, ei lisäksi Windowsiin. Kahdella asennuksella on eri asetukset ja eri kirjautumistiedot, ja on helppo kirjautua toisessa ja ajaa komentoja toisessa.
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 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:
- Branding: anna sovellukselle nimi, esimerkiksi TIES4080 ohjaus4, ja tukisähköpostiosoite. Vain pakolliset kentät tarvitsee täyttää.
- Audience: valitse käyttäjätyypiksi External. Internal toimisi vain saman organisaation tunnuksilla.
- Data Access: lisää oikeudet
openid,…/auth/userinfo.emailja…/auth/userinfo.profile. Muita oikeuksia ei tarvita. Nämä kolme kuuluvat Googlen ei-arkaluonteisiin oikeuksiin, joten sovellus ei tarvitse Googlen tarkastusta. - 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.
- Clients → Create client: valitse tyypiksi
Web application ja täytä osoitteet:
- Authorized JavaScript origins:
http://localhost:8080ja myöhemmin Cloud Run -palvelusi osoite, esimerkiksihttps://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/authja 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.
- Authorized JavaScript origins:
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.
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:
- Edition: Standard
- Mode: Firestore Native — tällä kurssilla ei käytetä Datastore-tilaa
- Security rules: Restrictive. Säännöt koskevat vain selaimesta ja mobiilisovelluksista tulevia pyyntöjä. Palvelinsovellus käyttää tietokantaa palvelutilin oikeuksilla, joten rajoittavat säännöt eivät estä sitä.
- Location: Region,
europe-north1 (Finland)
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
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:
- Add data to Cloud Firestore
- Get data with Cloud Firestore
- Perform simple and compound queries
- Order and limit data
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
- Lisää Flask-sovellukseen sivu, joka listaa reseptit ruokalajeittain yllä olevan mallin mukaan.
- Listaa reseptien yhteydessä myös niiden henkilömäärät.
- Tee WTForms-lomake ruokalajien lisäämiseen. Kun lisääminen onnistuu, muuta lomake muokkauslomakkeeksi ja tee ruokalajilistauksesta linkkejä, joilla muokattava ruokalaji valitaan.
-
Tee vastaava lomake resepteille. Käytä muotoa
/reseptit/ruokalajin-id/reseptin-idolevia 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.
- 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():
...
@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.
- Julkinen pääsy (Allow public access,
--allow-unauthenticated): web-sovellus on yleensä julkinen. - Laskutusmalli: request-based laskuttaa vain siltä ajalta, kun sovellus käsittelee pyyntöjä, instance-based koko instanssin käynnissäoloajalta. Pienelle web-sovellukselle request-based on lähes aina halvempi.
- Instanssien vähimmäismäärä (
--min-instances): pidä arvona 0, jolloin sovellus sammuu, kun käyttöä ei ole. Ensimmäinen pyyntö sammumisen jälkeen on hitaampi, koska instanssi pitää käynnistää. - Instanssien enimmäismäärä (
--max-instances): aseta aina, jotta kustannukset eivät karkaa, jos käyttäjiä tulee yllättäen paljon. - Muisti ja prosessori (
--memory,--cpu): oletus on 512 MiB ja yksi prosessori. Käytä tällä kurssilla 256 MiB:tä, joka riittää pienelle sovellukselle. Alle 512 MiB:n muisti edellyttää ensimmäisen sukupolven suoritusympäristöä (--execution-environment gen1). Muistirajan ylittävä instanssi lopetetaan, joten jos lokeissa näkyy muistiin liittyviä virheitä, kasvata muistia. - Aikakatkaisu: oletuksena oleva 300 sekuntia on sopiva.
- Samanaikaiset pyynnöt (concurrency): oletuksena yksi instanssi käsittelee enintään 80 pyyntöä yhtä aikaa. Mitä suurempi arvo, sitä pidempään yksi instanssi riittää ennen uuden käynnistämistä. Asynkroninen sovellus, esimerkiksi Quartilla toteutettu, voi hyötyä suuremmastakin arvosta.
- Startup CPU boost antaa instanssille lisää prosessoritehoa käynnistyksen ajaksi, mikä voi nopeuttaa kylmäkäynnistyksiä.
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
Käyttäjien kommentit