Yleistä
Silloin joskus, kauan aikaa sitten, kun aloittelin tietokoneiden kanssa puljaamista, niin normaali toimintatapa oli ottaa pääteyhteys johonkin (ainakin fyysisiltä mitoiltaan) suureen keskustietokoneeseen. Sen käytöstä maksettiin sitten käytetyn CPU-ajan perusteella.
Sitten tulivat PC-tietokoneet (Personal Computer) ja keskustietokoneet jäivät taka-alalle normaalista päivittäisestä käytöstä. Jokaisella piti tietenkin olla oma tietokone, joka toi ongelmia päivitysten ja tiedonhallinnan kannalta. Tiedot olivat levällään missä sattuu ja varmuuskopiot olivat harvinaisuus. Tietoturvasta ei oltu kuultukaan.
Ongelmia yritettiin ratkaista palvelimilla, joilla tiedot olisivat säännöllisessä varmuuskopioinnissa ja käyttäjäoikeuksien takana. Se onnistui joskus paremmin, joskus huonommin. Lopputulos oli kuitenkin se, että nyt tietoja oli sekä henkilökohtaisilla koneilla, että palvelimilla. Samoin laiteresurssit (laskentateho, tallennustila) olivat jakautuneet. Ei ihan ongelmaton tämäkään, ei ainakaan resurssien tehokkaan käytön kannalta. Lisäksi kustannukset olivat siirtyneet alkuaikojen muuttuvista kustannuksista (maksettiin käytöstä) enemmän kiinteiden kustannusten puolelle (maksetaan laitteista ja tilasta). Talousihmisten painajainen: kiinteät kustannukset kasvavat.
Nyt on (tulossa) serverless. Mikäs se sitten on? Se ei tarkoita, että ei olisi ollenkaan palvelimia, vaan että ei ole omia palvelimia. Toimintatapana ja kustannusmielessä siis paluu keskustietokoneaikaan. Paitsi, että nyt ei ole suuria keskutietokoneita vaan suuria palvelinkeskuksia (datacenter).
Serverless voi ratkaista monia ongelmia: päivitykset, varmuuskopiointi, laiteresurssit, käyttövarmuus,... Tietoturvaihmiset tosin panikoivat, kun data ei ole enää "turvallisesti" omien seinien sisällä. No se on heidän ongelmansa, ei minun.
Mitäs etua tämä tuo sitten pienelle (IoT-)kotikäyttäjälle? Tietenkin pienemmät kustannukset, kun maksetaan (vain) käytöstä. Pieni käyttö, pienet kustannukset.
Halvinkin 24/7-palvelin maksaa $3,5/kk, kun taas pieni serverless-yksikkö on $0,0000017/s + $0,2/1e6 kutsua. Sen lisäksi tarvitaan API Gateway ohjaamaan liikennettä. Sen hinta on $1.06/1e6 kutsua. Tuosta nopeasti päässälaskien serverless IoT:n 24/7 selainliityntä maksaa pienelle kotikäyttäjälle pyöristettynä noin suunnilleen ei mitään.
AWS DynamoDB-tietokannan osalta pieni (?) kotikäyttö ei ylitä hinnoittelukynnystä: 25GB storage, ~200e6 kutsua/kk.
IoT:n MQTT-välityspalvelu maksaa: $0.096/1e6 yhteysminuuttia + $1.20/1e6 viestiä. (Hinnat alv0 AWS 01/2024)
Pienellä kotikäytöllä näistä ei saa mitenkään muodostumaan kovin isoa laskua. Yksi IoT-laite, joka lähettää viestin 15min välein ja 100 selaimella tehtyä tietokantakyselyä vuorokaudessa tekee ~2cent/kk. Ei luulisi kenekään talouden tähän kaatuvan!
Serverless IoT
Lämpötilan mittauksessa on seuraavanlainen IoT-tietoketju:
- Lämpötila mitataan IoT-laitteella (Raspberry Pi) ja lähetetään 15min välein IoT-välityspalvelimelle (AWS IoT Core).
- IoT-välityspalvelimella on ohje (rule), joka tallentaa mittaustiedon AWS DynamoDB-tietokantaan.
- Tietojenkäsittely-yksikkö (AWS Lambda) lukee yhden vuorokauden mittaustiedot tietokannasta ja näyttää ne selaimella.
Tietoketjun kaksi ensimmäistä kohtaa on tehty jo aikaisemmin. Niissä molemmissa käytetään serverless-palvelua: AWS IoT Core ja AWS DynamoDB. Jäjelle jää viimeinen eli mittaustietojen näyttö selaimella. Se tehdään AWS Lambda-tietojenkäsittely-yksiköllä (compute), jolle verkkoliikenne ohjataan AWS API Gateway'llä. Molemmat palvelut ovat tyyppiä serverless.
AWS Lambda
Ohjelma hakee mittaustiedot DynamoDB-tietokannasta, lisää ne HTML-pohjaan, index.html, ja palauttaa täydennetyn sivun selaimelle. Ensimmäisellä kerralla sivulla ovat viimeisen vuorokauden mittaustulokset. Sivulla on valinta, jolla voi muuttaa hakupäivämäärän ja tehdä sitten uuden tietokantahaun.
Lambda-functio
Tehdään uusi Lambda-functio:
- GetTempsFromDynamoDB
- Python 3.12, arm64
- httpapi-lambda-dynamodb-role
- httpapi-lambda-dynamodb-policy
httpapi-lambda-dynamodb-policy:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "Stmt1428341300017", "Action": [ "dynamodb:DeleteItem", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:UpdateItem" ], "Effect": "Allow", "Resource": "*" }, { "Sid": "", "Resource": "*", "Action": [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], "Effect": "Allow" } ] }
Lisätään function code-sivulle tiedostot index.html ja lambda_function.py.
index.html:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html lang="fi"> <head> <title>Mitatut lämpötilat</title> <meta name="generator" content="Bluefish 2.2.12" > <meta name="author" content="esko" > <meta name="description" content="Mitatut lämpötilat AWS DynamoDB:stä"> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <style> table { text-align: center; border: 2px solid black; } th, td { border: 2px solid black; padding: 5px; padding-right: 20px; padding-left: 20px; } </style> </head> <body> <div align="center"> <h1>Mitatut lämpötilat</h1> <form method="post"> <label for="date">Valitse päivä</label> <br><br> <input type="date" name="date" value="{-Today-}"><br> <br><br> <input type="submit" name="send" value="Submit"> <br><br> <div> <h2>Mittausarvot</h2> <br> <table> <th>Paikka</th><th>Aika</th><th>Lämpötila</th> {-MeasTable-} </table> </div> </div> </body> </html>
lambda_function.py:
""" Lukee yhden vuorokauden lämpötilamittaukset AWS DynamoDB:stä ja lisää ne html-pohjalle index.html. Täydennetty pohja palautetaan selaimelle. Jos päivämäärää ei ole annettu, ohjelma palauttaa viimeisen vuorokauden lämpötilamittaukset. """ import re import base64 import datetime import time import boto3 from boto3.dynamodb.conditions import Key, Attr # AWS DynamoDB-liityntä dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('temp_data') # Pääohjelma def lambda_handler(event, context): print(f"Lambda: GetTempsFromDynamoDB") with open("index.html", "r", encoding="utf-8") as f: html = f.read() # HTML-pohja parms = cmdparams(event) if "POST" in parms: dtiso = parms["POST"]["date"] dtiso += "T23:55:00+02:00" dt = datetime.datetime.fromisoformat(dtiso) end = datetime.datetime.timestamp(dt) else: end = time.time() start = end - 24 * 3600 end *= 1000 # Tietokannassa aika on ms start *=1000 end = int(end) start = int(start) # Haetaan tiedot tietokannasta response = table.scan( FilterExpression=Attr('sample_time').between(start, end) ) items = response['Items'] if len(items) > 0: # Tiedot LISTaan järjestämistä varten meastable = [] for item in items: meastable.append([ item["sample_time"], item["device_data"]["Loc"], item["device_data"]["Time"], item["device_data"]["Temp"] ] ) meastable.sort(reverse=True) # LISTa aikajärjestykseen # Tiedoista HTML-taulukko tablestr = "" for row in meastable: tablestr += "<tr>" tablestr += "<td>" + row[1] + "</td>" tablestr += "<td>" + row[2] + "</td>" tablestr += "<td>" + row[3] + "</td>" tablestr += "</tr>" else: tablestr = "Ei mittaustietoja!" # Lisätään tiedot HTML-pohjaan tod = datetime.date.today() iso = tod.isoformat() tmp = re.sub("{-Today-}", iso, html) # Oletuspvm htmluusi = re.sub("{-MeasTable-}", tablestr, tmp) # Mittaustiedot return { "statusCode": 200, "headers": { 'Content-Type': 'text/html' }, 'body': htmluusi } # Palauttaa html-sivun GET- ja POST-parametrit: # queryStringParameters (GET) tai # body (POST) def cmdparams(event): method = event["requestContext"]["http"]["method"] parms = {} match method: case "GET": if "queryStringParameters" in event: parms = {"GET": event["queryStringParameters"]} case "POST": if event["isBase64Encoded"]: tmp = base64.b64decode(event["body"]) tmp = tmp.decode("utf-8") tmp = re.split("&", tmp) else: tmp = event["body"] tmp = re.split("\s+", tmp) if tmp[-1] == "": tmp = tmp[:-1] resp = {} for pair in tmp: par = re.split("=", pair) resp[par[0]] = par[1] parms = {"POST": resp} case _: parms = {"ERROR": "Unknown method"} print(f"Parms: ", parms) return parms
AWS API Gateway
Selaimen sivuhaku ohjataan HTTP API:lla Lambda-functiolle. HTTP API välittää sivun GET- ja POST-tiedot functiolle ja palauttaa vastauksen selaimelle.
HTTP API
Tehdään uusi HTTP API:
- GetTempsFromDynamoDB
- Integrations
- Lambda: AWS Region, Lambda function, Version 2.0
- Configure routes
- ANY, / -> GetTempsFromDynamoDB
- Configure stages
- $default (Auto-deploy: enabled)
Tämän HTTP API:n Invoke URL-osoitteesta löytyy sitten edellisellä Lambda-functiolla tehty mikro-palvelu. (micro service) (Esim.)