SSL-protokoll for web-applikasjoner på web.py

 155 total views

SSL (TLS) -protokoll er svært vanlig i bruk for de fleste web-stedene i Internet. Om du lager en web-applikasjon bør du sette deg inn i hvordan du konfigurerer den for bruk med SSL. Om du bruker web.py for å lage web-applikasjoner i Python, får du en veiledning i denne artikkelen.

(c) 16.01.2024 Anders Fongen

Innledning

Det første leddet i en web-adresse (URL) angir hvilken protokoll som brukes. Oftest står det https:// der, sjeldnere http://. Den første formen forteller web-leseren at forbindelsen til web-tjeneren skal bruke SSL-protokoll (også kalt TLS) for å beskytte mot avlytting og forfalskning.

Jeg gir ingen detaljert beskrivelse av SSL her, fordi det finnes andre steder på Internet, og fordi det ikke er av vesentlig betydning å kjenne detaljene i denne protokollen.

SSL gir kryptert overføring av data, slik at andre ikke kan lese innholdet av dataoverføringen om de lytter til kanalen som brukes. IP-adresser til sender og mottaker er derimot ikke kryptert, og kan oppfattes gjennom avlytting.

SSL gir også autentisering av én eller begge parter i dataoverføringen, slik at det er vanskelig å utgi seg for å hete f.eks. www.vg.no uten å presentere et digitalt nøkkelsertifikat som attesterer at det er web-tjenerens virkelige navn.

Mindre brukt, men allikevel viktig, er at SSL også kan autentisere web-leseren ved hjelp av et digitalt sertifikat, som attesterer at brukeren av web-leseren heter f.eks. Anders Fongen. Denne funksjonen i SSL kan erstatte tradisjonell innlogging med brukernavn og passord.

Bruk av sertifikater og nøkler, og hvordan sertifikater kan validere andre sertifikater, er et noe omfattende tema som jeg heller vil behandle i en egen artikkel. Begrepet kalles Public Key Infrastructure, og det finnes mye informasjon om dette på Internet.

Jeg har i en tidligere artikkel vist en web-applikasjon bygget på web.py-rammeverket og programmert i Python. Denne artikkelen vil videreføre det samme fokus på rammeverk og programmeringsspråk, og vise hvordan SSL-protokoll konfigureres i en web-applikasjon bygget på web.py.

Nøkkelpar og sertifikater

For å konfigurere en web-tjener for SSL-protokoll trenger den et nøkkelpar. Det kan utstedes av en kommersiell leverandør, noe som koster en del (fra 50€/år og oppover). Med et slikt nøkkelpar kan din web-tjener autentisere seg overfor alle klienter i Internet. Det er derimot ikke praktisk mulig å be alle brukerne av web-applikasjonen din om å betale et slikt beløp for å oppnå klient-autentisering.

For web -applikasjoner som skal brukes innenfor en gruppe, firma, forening etc., er det hensiktsmessig å selv stå for utstedelsen av nøkkelpar og sertifikater. Alle som skal bruke den aktuelle web-tjeneren må da installere et sertifikat og et nøkkelpar i sin egen web-leser, og jeg skal vise hvordan dette gjøres i Google Chrome. For Firefox er fremgangsmåten ganske lik.

Bruk helst en Linux-maskin som web-tjener, da fungerer den presenterte metoden best. Her følger et skall-skript som bruker programmet openssl til å lage et rotsertifikat (også kalt CA-sertifikat), og nøkkelpar for web-tjeneren og et nøkkelpar for én klient. Skallskriptet er ment som et eksempel som du modifiseres etter eget ønske.

# Generate keys and certificates for OpenVPN
openssl genrsa -out ca_key.pem 2048
openssl genrsa -out server_key.pem 2048
openssl genrsa -out client_key.pem 2048
#  Generate CA certificate
openssl req -new -x509 -days 1000 -key ca_key.pem -out ca_cert.pem \
    -outform PEM -subj '/C=NO/O=FONGEN/CN=CA'
#  Generate Server certificate
openssl req -new -key server_key.pem -out server_csr.pem \
    -subj '/C=NO/O=FONGEN/CN=SSLSERVER' \
    -addext 'subjectAltName = DNS:ssl.fongen.no'
openssl x509 -req -in server_csr.pem -CA ca_cert.pem -CAkey ca_key.pem \
    -CAcreateserial -addtrust serverAuth -out server_cert.pem \
    -days 1000 -sha256 -copy_extensions copy
openssl verify -CAfile ca_cert.pem server_cert.pem
#  Generate Client certificate
openssl req -new -key client_key.pem -out client_csr.pem \
    -subj '/C=NO/O=FONGEN/CN=AndersFongen'
openssl x509 -req -in client_csr.pem -CA ca_cert.pem -CAkey ca_key.pem \
    -CAcreateserial -addtrust clientAuth -out client_cert.pem \
    -days 1000 -sha256
openssl pkcs12 -export -inkey client_key.pem -in client_cert.pem \
    -out client_keypair.p12
openssl verify -CAfile ca_cert.pem client_cert.pem
#  Remove unneccesary files
rm *_csr.pem

Når dette skallskriptet kjøres (du blir bedt om å skrive et passord underveis, det skal brukes når du installerer klientens nøkkel i web-leseren) skapes det følgende filer på samme katalog:

  • ca_key.pem – Dette er utstederens private nøkkel og skal aldri kopieres eller sendes til andre
  • ca_cert.pem – Dette er utstederens nøkkelsertifikat som skal installeres både i web-tjeneren og i web-leseren
  • server_key.pem, server_cert.pem – Dette er web-tjenerens private nøkkel og nøkkelsertifikat som som skal installeres der, men ikke gis til web-leserne
  • client_keypair.p12 – Dette er nøkkelen og sertifikatet til én klient, beskyttet med det passordet du nettopp skrev inn.

Installasjon av nøkler og sertifikater i Google Chrome

  • I Google Chrome, klikk på knappen med “tre prikker i vertikal ordning” øverst til høyre
  • Velg så “Settings” fra menyen
  • I listen til venstre, klikke “Privacy and Security”
  • Klikk deretter på “Security”, så på “Manage Certificates”
  • Klikk på fanen “Authorities”, og så på knappen “Import”
  • Fra den fildialogen du nå får se, velg filen “ca_cert.pem”
  • Etter at import-jobben er fullført skal du nå se dette rotsertifikatet i listen. Navnet som vises er “org-FONGEN” dersom du ikke har endret dette i skallskriptet (noe du bør gjøre).

Nå skal klientsertifikatet og nøkkelen installeres.

  • Klikk på “Your Certificates” og deretter på knappen “Import”.
  • Fra fildialogen, velg “client_keypair.p12”.
  • Du må nå skrive inn passordet som du valgte da sertifikatet ble laget for litt siden.
  • Nå skal sertifikatet vises i listen med det navnet som du har valgt å bruke.

Installasjon av nøkler og sertifikater i web-tjeneren

Nå er web-leseren klargjort, og vi skal sette opp web-tjeneren med sine nøkler og sertifikater:

from cheroot.server import HTTPServer
from cheroot.ssl.builtin import BuiltinSSLAdapter
import web, ssl

ssl_adapter = BuiltinSSLAdapter(
	certificate = 'server_cert.pem',
	private_key = 'server_key.pem',
	certificate_chain = 'ca_cert.pem')

#ssl_adapter.context.verify_mode = ssl.CERT_REQUIRED
ssl_adapter.context.verify_mode = ssl.CERT_OPTIONAL
HTTPServer.ssl_adapter = ssl_adapter

urls = ("/.*", "hello")
app = web.application(urls, globals())

class hello:
	def GET(self):
		try:
			cn = web.ctx.env['SSL_CLIENT_S_DN_CN']
		except:
			return "No client certificate available"

		return "Hello, " + cn

if __name__ == "__main__":
	app.run()

Jeg har laget en demonstrasjonsvideo som tar deg gjennom disse stegene enkeltvis, og også hvordan web-tjeneren startes og testes. Du kan se den her:

Demonstrasjon av SSL-autentisering i web.py

Takeaway

Avslutningsvis i denne artikkelen vil jeg påpeke at en del av dette stoffet også er til nytte ved bruk av andre web-rammeverk enn web.py. De fleste web-tjenere har støtte for klientautentisering med SSL, og de konfigurerer dette på forskjellige måter. Men laging av sertifikater og nøkler, samt installasjonen av klientnøkler i web-leseren, er de samme operasjonene, derfor kommer det presenterte skallskriptet til nytte.

CMS – Et lettvekts samarbeidssystem for mindre brukergrupper

 118 total views,  1 views today

Visst har vi Teams og andre skybaserte systemer for å samarbeide gjennom nettet, men vi har også behov for noe enklere for små grupper med veldefinerte samarbeidsmodeller. Vi skal vise et slikt system her, og påpeke hvilke fordeler som følger av et kompakt og fokusert system.

CMS-demonstratoren kan kjøres fra adressen http://hos.fongen.no/cms
Kildekoden i arkivform kan lastes ned fra denne lenken

(c) Anders Fongen, 2024

Innledning

Se for deg en gruppe med noen titalls medlemmer, som skal samarbeide om en fokusert og veldefinert oppgave. F.eks planlegging og utforming av et undervisningsfag, en leteoperasjon etter savnede personer, en oppgave knyttet til kartlegging og etterretning, skriving av sakprosa, en chat.

Slike oppgaver er kjennetegnet ved visse egenskaper og behov:

  • Enkle behov for typografisk formatering av tekst
  • Behov for enkelt å opprette nye dokumenter, og endre på eksisterende
  • Høy tillit mellom deltakerne, med felles fokus og nødvendig kompetanse
  • Behov for raskt å gjøre seg kjent med andres bidrag til en aktivitet
  • Enkelt å fjerne utdatert, irrelevant og distraherende innhold

For slike oppgaver er det kanskje fristende å ta frem et generisk samarbeidverktøy med stort behov for styring og konfigurasjon, med høye kapasitetskrav til utstyr og kommunikasjonskanaler. F.eks. Teams, DocuLive, Itslearning, Canvas, Google Disk. Disse dekker muligens behovet, men ikke nødvendigvis på den mest effektive måte, de kan nelig fremstå som en “skyte spurv med kanoner”-løsning. Dessuten trenger de en internett-forbindelse til en skytjeneste, bare så det er sagt.

Vi skal i denne artikkelen presentere et enkelt samarbeidssystem kalt CMS – Content Management System. CMS kjennetegnes ved følgende egenskaper:

  • Minimalt med programkode, < 300 linjer i Python
  • Bruker markdown syntaks for tekstformatering
  • Web brukergrensesnitt (krever kun en web-leser)
  • Dokumentlås som virker
  • Enkel mekanisme for adgangskontroll
  • Dokumenter kan “oppfriskes” automatisk av web-leseren
  • Dokumenter kan opprettes og redigeres gjennom MQTT-protokoll
  • OpenSource, enkel kode for enkel utvidelse og endring

La oss i resten av dette dokumentet presentere disse egenskapene enkeltvis, og forklare hvordan de støtter aktuelle brukstilfeller. En grundigere presentasjon ligger i selve CMS-systemets demonstrasjonstjener på http://hos.fongen.no/cms.

Agil utviklingsmetodikk og Wiki

Behovene som ble listet opp i innledningen minner mye om egenskaper ved moderne system- og programutvikling, kjent som “agile development”. Gå gjerne via denne lenken for å lese litt om “agile” metoder (“agil” har i mellomtiden rukket å bli et norsk ord med norsk uttale).

For å støtte agilt samarbeid utviklet Ward Cunningham i 1995 en dokumentlagringstjeneste kalt Wiki. Med en Wiki kan enhver bruker opprette, redigere og slette dokumenter, altså et fravær av adgangskontroll som kun fungerer i grupper med felles mål og metode. En Wiki kan skrives kompakt med kun noen titalls programlinjer, men uten en redaktør som kan jobbe som “husmor” er de fleste erfaringer den at dokumentsamlingen med tiden blir en uhåndterlig blanding av nyttige, feilaktige, overlappende og utdaterte dokumenter. Wiki-prinsippet finnes også i Wikipedia, men her er det ansatt husmødre som overvåker redigeringen og opprettelsen av nye sider.

Den positive erfaringen med Wiki-prinsippet at en “90%”-løsning, dvs. 90% av behovene til en samarbeidsgruppe, lar seg løse med enkel og kompakt programmering, mens en 99%-løsning er kanskje 100 ganger større og dyrere. Såkalt “feature creep” i systemspesifikasjonene, dvs. at stadig nye funksjonskrav blir lagt til underveis, er en svært dyr uvane, og en fokusert og nøktern “90%”-løsning kan gi mye bedre effekt av innsatsen.

Overordnet beskrivelse av CMS

Dette er en web-tjeneste programmert i Python over rammeverket web.py, og som lar en gruppe av brukere søke, opprette og redigere dokumenter direkte fra sin web-leser. Den skiller mellom “lese”- og “redigere”-brukere slik at kun sistnevnte kan endre innholdet av dokumentsamlingen.

Brukerkontroll

Skillet mellom de to brukergruppene (“lese” og “redigere”) kan gjøres på mange måter, men for å unngå egne tabeller over brukernavn og passord (vi har nok av passord å huske på allerede) har jeg testet ut to metoder: IP-adresser eller klient-sertifikater:

IP-adresser: Bare klienter med IP-adresser som brukes bak brannveggen gir redigeringstillatelse, f.eks. IP-adresser som begynner med 192.168.x.x. Klienter forespørsler fra Internet vil aldri bruke disse adressene. Klienter tilkoplet via Virtuelle Private Nett (VPN) vil derimot kunne bruke adresser som gir redigeringstillatelse.

Klient-sertifikater: Der hvor det benyttes TLS (https:…) protokoll mellom klienten og tjeneneren kan TLS også sørge for at klientmaskinen må fremvise et nøkkelsertifikat og demonstrere sin private nøkkel for å autentisere seg. Kun de klientene som gjennomfører autentisering får redigeringstillatelse, alle andre kun lesetillatelse. Bruk av klient-sertifikater har den fordel at brukerne blir identifisert individuelt og man kan loggføre den enkelte brukers aktiviteter.

Dokumentorganisering

Dokumentene i CMS er hierarkisk organisert omtrent slik som i et filsystem, men istedetfor filkataloger brukes “mor-dokumenter”. Ethvert dokument kan være “mor” til et antall underordnete dokumenter. De underordnete dokumentene med felles mor kalles “søsken”. Når et dokument vises på skjermen vil både de underordnede dokumentene og søsken-dokumentene vises på et sidepanel. CMS inneholder også en enkel søkefunksjon hvor det kan søkes etter ordforekomster i tittel og innhold.

Dokumentene i CMS ligger lagret i en database, ikke som separate filer. Det åpner for en rekke muligheter for å sortere og søke i dokumenter, etablere reasjoner mellom dokumenter, låse dokumenter under redigering osv. Det åpner også for muligheten for andre programmer å skrive direkte til databasen for f.eks. å opprette dokumenter automatisk. Dette er en viktig egenskap ved CMS som vi skal omtale senere.

Redigering med MarkDown

De fleste er vant til å se en ferdig layout når dokumentet redigeres, på den fom det vil bli presentert for en som leser det. Det finnes andre måter å redigere på hvor layout påføres dokumentet som instruksjoner i tekstformat. Hypertext Markup Language (HTML) er det mest kjente eksemplet, hvor f.eks. teksten “<h2>” ikke vises i web-leseren, men indikerer at påfølgende tekst skal vises som en overskrift. Latex er et velkjent system for fremstilling av vitenskapelige artikler som benytter det samme prinsippet.

En av fordelene med markup er at instruksjonene indikerer intensjonen til forfatteren, ikke nøyaktig hvordan utseendet skal bli. “<h2>” kan resultere i ulik utseende, men alltid i form som indikerer en overskrift. En fargeskriver kan gi overskriften en annen uforming enn en svart-hvitt skriver f.eks.

Markdown er et “slenguttrykk” for et markup-språk som produserer HTML-kodet innhold. Markdown er ofte et enkelt sett av instruksjoner i en kort og enkel form, som f.eks. _underlinjert_ eller **uthevet** skrift. Når dokumentet blir presentert vil disse kodene være erstattet av den ønskede layouten. Wiki-systemene har ofte benyttet en form for markdown ved redigering.

Den viktigste egenskapen ved markdown er derimot at et slikt manuskript føles enkelt og naturlig å lese, også uten omforming av instruksjonene. Her er noen eksempler på tekst skrevet med HTML, Latex og Markdown, for å vise at sistnevnte er lettest å lese. Et dokument som ser slik ut:

Kan skrives med henholdsvis HTML, Latex og Markdown på denne måten:

Det er lett å konstatere fra dette eksemplet at Markdown er lettere å skrive inn, og lettere å lese i manuskriptform. I andre enden finner vi Latex, med kraftige mekanismer for å organisere layout og struktur på dokumentet, men som også er tung å skrive inn og vanskelig å lese i manuskriptform.

Markdown kan behandles av programkode for å f.eks. fremstille HTML kode for fremvisning i en web-leser. Med enkle regler kan Markdown behandles med såkalt regulære uttrykk i mange programmeringsspråk, men det finnes også egne programbiblioteker og ferdige programmer som kan gjøre dette. Dette er “kjedelig” programmeringsarbeid og en jobb som er godt å slippe.

Programmet heter pandoc, og finnes i versjoner for Mac, Linux og Windows. Det finnes også et Python-bibliotek kalt pypandoc som tilbyr et utvalg av pandoc’s muligheter gjort tilgjengelig for et Python-program.

Låsing av dokumenter under redigering

Kan flere brukere redigere samme dokument samtidig? I prinsippet ja, og Google Drive tillater nettopp det, men da må de i fellesskap sørge for at dokumentets innhold forblir meningsfylt og sammenhengende. I de fleste tilfellene er det tilstrekkelig løsning å la kun én bruker redigere, og andre som også ønsker dette, må vente på tur. Microsoft Word har lenge hatt en slik “lås”, som åpnes først når dokumentet lukkes etter bruk.

Dette har aldri fungert godt! Alle som har redigert med Word eller Excel har opplevd at en kollega har gått hjem for dagen uten å lukke dokumentet, som da forblir låst inntil neste dag.

I CMS er det valgt en låsemekanisme som automatisk åpner seg etter en viss tid (5 minutter), eller når dokumentet lukkes etter redigering. Dette er et kompromiss: Det løser problemet med gjenglemte låser, men gir brukeren 5 minutter på å fullføre redigeringen (må da gå ut av redigeringen og inn igjen). Dette skjer ved et triks i databasen som også sikrer at flere ikke kan låse et dokument samtidig. Sjekk kildekoden for å se hvilken SQL-syntaks som brukes.

Overvåking av dokumenter

Alle dokumenter i databasen er påført en “refresh rate”, dvs. et tidsrom mellom hver gang dokumentet skal hentes på nytt av web-leseren. Om “refresh rate” er satt til 10 sekunder, vil endringer i dokumentet (gjort av andre) vises frem i alle web-lesere som ser på dette dokumentet senest etter 10 sekunder. Slik kan f.eks. en situasjonsrapport alltid bli vist med de siste oppdateringene uten at brukerne på gjøre en “reload” av dokumentet.

Denne mekanismen introduserer en nødvendig balanse mellom akseptabel forsinkelse i oppdateringen av andre klienter, og den nettverkstrafikken som oppstår knyttet til den stadige hentingen av dokumenttdata. Settes “refresh rate” til 0 skjer ingen slik oppdatering automatisk.

Bruk av bilder

Bilder kan settes inn i dokumentene som redigeres ved å skrive inn en instruksjon i den løpende teksten: <img src=Pictures/filnavn.jpg>. Dette er den vanlige HTML-kommandoen som brukes for dette formålet, og ekstra parametre for å justere størrelse kan også brukes.

Alle bilder legges på samme katalog (Pictures) på CMS-tjenerens filområde, og nye bilder kan lastes opp i PNG eller JPG-format ved å klikke på en knapp i det høyre sidepanelet. Pass på at bildene får unike filnavn, og bilder som ligger der kan inkluderes i mange dokumenter.

Automatiske dokumenter – Internet of things

Automatiske dokumenter betegner dokumenter som legges inn i databasen uten medvirkning fra brukerne av systemet. Disse dokumentene vises frem som andre dokumenter, og egner seg for å vise data fra miljøsensorer, web-kameraer, mashup fra internettkilder (værmelding, trafikkrapporter, børsdata) osv. Et automatisk dokument kombinert med bruk av “refresh rate” kan vise et situasjonsbilde eller en operasjonell status i tilnærmet sann tid, og oppdateres automatisk.

  • Dokumenter som legges inn av brukere kan ikke overskrives eller endres av denne automatiske prosessen.
  • Automatiske dokumenter kan ikke redigeres eller slettes av brukere

Automatiske dokumenter utvider bruksområdet til CMS kraftig, utover det å være en interaktivt dokumentarkiv. CMS kan dermed inkludere og vise tilstanden i omgivelsene. Dette er en kopling til det som kalles Internet-of-things.

I den foreliggende programkoden finnes modulen PubSubAdapter.py som kopler automatiske dokumenter til Publish-Subscribe distribusjon og MQTT-protokoll. På denne måten kan CMS være en del av et større nettverk av noder som samarbeider om å behandle sensor-information. Modulen createCMS doc.py gir et eksempel på hvordan MQTT-protokollen kan brukes for å opprette eller endre et CMS-dokument.

Legg merke til at programkoden i PubSubAdapter.py også tillater at tekst bli lagt til i eksisterende dokumenter, ved å søke etter tegnstrengen <!-- Insert --> i det eksisterende dokumentet. Denne tegnstrengen blir da erstattet med det nye innholdet, og dokumentet forøvrig blir bevart.

Egen kjøring, videre utvidelser og eksperimentering

Linken øverst i artikkelen laster ned et tar-arkiv av kildeteksten. Jeg anbefaler å kjøre CMS-tjeneren på en Linux-maskin, da den ikke er testet under Windows.

Last ned kildekoden i tar-format fra lenken i toppen av artikkelen, og hent frem cms.py i en editor. Du må se over definisjonene i begynnelsen av filen, spesielt om verdien av innerIP svarer til IP-adressene i ditt lokalnett.

Om du vil at CMS skal være tilgjengelig for omverdenen må du konfigurere routeren din med port forwarding, og kanskje satt opp dynamisk dns-navn. Dette omtales ikke her.

Før du starter CMS må disse programmene være installert:

  • Programmene pandoc, sqlite3
  • Python-bibliotekene web.py, pypandoc, paho-mqtt

Start så det hele med kommandoen

python3 cms.py 8080

8080 betegner TCP-porten som CMS skal bruke, om du endrer denne til 80 må du huske på å være root-bruker.

CMS har muligens ingen funksjoner som ikke finnes i andre samarbeidssystemer, men har det fortrinn at det er enkelt og kompakt, med kun noen hundre programsetninger. Systemets konstruksjon er dermed lett å forstå, utvide og endre. Det er derfor mitt ønske at de som laster ned og starter opp CMS vurderer utvidelser og nye funksjoner.

Fletrådsprogrammering i MicroPython

 257 total views

Anders Fongen, mars 2023

Dette innlegget handler om programmering med flere tråder i mikrokontrollere som bruker MicroPython. Det vil studere hvordan MicroPython kjører tråder i ESP32 og Raspberry Pico, og gi forslag til hvordan programmene kan gjøres sikre og raske.

Innledning

Noen programmer blir enklere å skrive dersom aktivitetene kan deles opp i uavhengige, samtidige aktiviteter. F.eks. kan et program trenge å reagere på trafikk fra nettverket samtidig som det skal betjene en brukerdialog. Alle moderne operativsystemer tillater at en prosess har flere slike utføringsaktiviteter som kalles tråder. Tråder skal derimot ofte samarbeide om deling og kommunikasjon av data, noe som krever at de benytter seg av synkroniseringsmekanismer. Dette temaet hører til fagfeltet Operativsystemer, og kan ikke diskuteres grundig i dette innlegget. Se kapitlene 6 og 7 i boka som jeg har skrevet om operativsyster. Den finner du her.

“Vanlig” Python har et omfattende programbibliotek for programmering med flere tråder, (en veiledning i bruken finnes f.eks. her). MicroPython, derimot, har et lite og enkelt bibliotek som tilbys på noen microkontrollere, bl.a. ESP32 og Raspberry Pico (ikke ESP8266). Derfor skal resten av innlegget studere forskjeller mellom disse to produktene, og foreslå programmeringsteknikker for programmer som vil kjøre riktig på begge to.

Merk at vi nå diskuterer hvordan tråder utføres i et MicroPython program. Med andre programmeringsspråk kan situasjonen være anderledes. Merk også at innlegget ikke beskriver hvordan PC og mikrokontroller settes opp for programutvikling. En veiledning for dette finner du f.eks. her (ESP32) og her (Raspberry Pico).

Bildet under viser hvordan de to mikrokontrollerne ser ut, og viser også hvorfor jeg foretrekker å bruke Raspberry Pico: ESP32 er noe bredere, og hindrer tilkoplingsledninger på den ene siden når den settes i eksperimentbrett (breadboard) som vist på bildet.

ESP32 (til venstre) og Raspberry Pico

Trådbehandling og CPU-kjerner

ESP32 og Raspberry Pico (heretter kun kalt Pico) har begge 32-bits registre i CPU (noe som gjør regneoperasjoner raskere) og to CPU-kjerner. Med flere CPU-kjerner kan samtidige oppgaver kjøres i separate CPU-kjerner og dermed utføres raskere enn om oppgavene utføres skiftevis på samme CPU-kjerne.

  • Pico kan utføre oppgaver i to tråder, som utføres i hver sin CPU-kjerne.
  • ESP32 kan utføre oppgaver i mange tråder (flere enn to), men alle kjører skiftevis på samme CPU-kjerne.

Hvordan kan vi fastslå dette? Se på programkoden nedenunder. Der kjører en oppgave i “hovedtråden”, og den samme oppgaven utføres i en “undertråd”. De representerer den samme arbeidsmengde og koden er laget kun for at det skal ta litt tid.

1 import _thread, time
2 def subthread():
3    for i in range(4000000):
4        x = 232**10
5    print("Undertråd ferdig")
6    
7 # Hovedtråd
8 x1 = time.time()
9 _thread.start_new_thread(subthread,())
10 for i in range(4000000):
11    x = 232**10
12 print("Hovedtråd ferdig")
13 x2 = time.time()
14 print("Kjøretid:", (x2-x1), "sekunder")

Dersom vi setter et kommentartegn ved linje 9 vil det ikke startes en undertråd, og vi ønsker å se om dette har noen effekt på den totale kjøretiden:

  • Dersom kjøretiden ikke endres, tyder det på at undertråden er blitt utført i en egen CPU-kjerne, som nå ikke lenger blir brukt.
  • Dersom kjøretiden (nær) halveres, tyder det på at begge trådene er blitt utført i samme CPU-kjerne, og at arbeidsmengden for denne nå er blitt halvert.
CPUmed undertråduten undertråd
Pico30 sekunder29 sekunder
ESP3237 sekunder22 sekunder
Kjøretid med to tråder, henholdsvis én.

Ut fra disse målingene konkluderer vi med at ESP32 kun benytter én CPU-kjerne, mens Pico benytter begge to.

Videre skal vi bruke det samme programmet for å studere antall tråder som kan kjøres på de to mikrokontrollerne. Ved å duplisere linje 9 slik at de skapes 2 undertråder, vil ESP32 utføre programmet korrekt (men med lengre kjøretid). Pico derimot, vil avbryte programmet med feilmeldingen “OSError: core1 in use“. Et program som skal være portabelt mellom Pico og ESP32 må derfor ikke lage mer enn én undertråd. Derimot vil Pico tilby raskere utføring av et slikt program, fordi begge CPU-kjernene tas i bruk.

Et program som skal vært portabelt mellom Pico og ESP32 må ikke lage mer enn én undertråd.

Synkronisering av tråder

I mange tilfeller vil trådene i et program samarbeide gjennom delte variabler, og de trenger å varsle hverandre om hendelser, f.eks. om en ny oppgave skal påbegynnes eller en annen er avsluttet og resultatet er klart. Tråder som ikke synkroniseres riktig kan skape såkalt Race Condition, som er en feilkilde som kan være vanskelig å spore opp fordi den ikke skaper feil hver gang. Man unngår Race Condition ved å reservere en ressurs (f.eks. ved endring av en variabel verdi) med en mekanisme som heter Lock (andre ganger kalt Mutex).

Tenk på Lock som en nøkkel du trenger for å låse opp en dodør. Når det er ledig på do henger nøkkelen på en krok utenfor døren. Du ønsker trolig å sitte i fred på do, og tar med deg nøkkelen inn dit. Etter bruk låser du døren og henger nøkkelen tilbake på kroken. Om ikke nøkkelen henger på kroken betyr det at doen er opptatt, og du venter utenfor til nøkkelen igjen dukker opp. Doen kan på denne måten bare brukes av én person, og om det er ledig går du inn uten venting.

Programlistingen under er eksempel på en Race Condition: To tråder tester og modifiserer den felles variabelen x, og dersom begge trådene finner at x==0, og deretter utfører x=x+1, vil verdien av x bli 2, selv om programkoden tilsynelatende ikke lar det skje. Feilen skyldes altså at “test-and-set” sekvensen (linje 10-13 og 20-23) ikke er atomisk, dvs. at andre tråder kan bruke den samme variabelen underveis i operasjonen.

For at programmet skal utføres riktig må vi låse test-and-set sekvensen, slik som vist i linje 9 og 19, og etterpå låse opp slik som vist i linje 14 og 24. Om du vil kjøre dette programmet på en mikrokontroller for å forvisse deg om dette, fjern kommentartegnet på disse linjene og kontrollere resultatet (som er fravær av feilmeldingen “Race Condition error…”).

1 # Race condition demo
2 import _thread, time
3 x=0 # Delt variabel
4 slock = _thread.allocate_lock() # Lås
5 def subthread():
6    global x, slock
7     while True:
8        # Lås for å fohinde race condition
9 #        slock.acquire() 
10         if x==0:
11             x=x+1
12         elif x==1:
13             x=x-1
14 #        slock.release() # Lås opp
   
15 # Hovedtråd
16 _thread.start_new_thread(subthread,())
17 while True:
18     # Lås for å fohinde race condition
19 #    slock.acquire() 
20     if x==0:
21         x=x+1
22     elif x==1:
23         x=x-1
24 #    slock.release()
25     if x<0 or x>1:
26         print("Race condition error, x=%d" % x)
27         break

Kommunikasjon mellom trådene

Tråder som løper uavhengig i et program gjør det oftest for å løse et problem, og må derfor kommunisere, ikke bare beskyttes mot Race Condition slik som vist ovenfor, én tråd skal f.eks. hente data fra en nettradio, og en annen tråd skal dekode mp3 og sende lyddata til en høyttaler via en DAC. I et slikt program må det foregå en synkronisert datastrøm mellom to tråder, som skal ha følgende egenskaper:

  1. Mottakertråden skal dekode dataene slik de mottas, og vente (blokkeres) i de periodene det ikke mottas data.
  2. Sendertråden skal sende data når det er mulig, og dersom lageret som brukes til overføringen går fullt, skal sendertråden vente på at mottakertråden leser data og frigjør plass.
  3. Data skal overføres First-in-First-out, dvs. mottas i samme rekkefølge som de sendes.

Det er mange programtyper som kan ha nytte av å la trådene inngå et såkalt produsent-konsument samarbeid, så her kommer noen klasser i MicroPython som fungerer som ønsket på både Pico og ESP32.

Condition klasse

Denne klassen har en oppførsel omtrent som en Lock, men kan “huske” et antall release-kall slik at mange acquire-kall kan slippe gjennom uten å blokkeres. Objekter av denne klassen kan faktisk erstatte Locks, men kan også brukes til mer. Her er programkoden:

import _thread
class Condition:
    def __init__(self,initvalue=0):
        self.fulfilled =  initvalue
        self.mutex = _thread.allocate_lock()
        self.sync = _thread.allocate_lock()
        self.sync.acquire()
        
    def wait(self):
        self.mutex.acquire()
        self.fulfilled -= 1
        while self.fulfilled < 0:
            self.mutex.release()
            self.sync.acquire()
            self.mutex.acquire()
        self.mutex.release()
            
    def notify(self):
        self.mutex.acquire()
        self.fulfilled += 1
        if self.fulfilled <= 0:
            self.sync.release()
        self.mutex.release()

notify-funksjonen ligner på Locks release-funksjonen, men Condition-klassen kan lagre antallet notify-kall, mens release-funksjonen kan ikke kalles på en Lock som ikke er “låst” (med acquire). Legg merke til variabelen fulfilled som holder rede på dette antallet, og at kall til wait-funksjonen teller ned den verdien og fortsetter dersom fulfilled er større enn null. Dersom verdien er >0 betyr det at den har noen notify-kall “til gode”, om verdien er <0 viser det antall tråder som blokkeres av wait-kall.

Operasjonene på fulfilled må beskyttes av en Lock (kalt mutex) for å unngå Race Condition.

FIFO klasse

FIFO-klassen har et lagringsområde for data som er sendt, men ikke mottatt (i variabelen storage), og funksjoner for å sende (put(object)) og motta (get()). For å synkronisere senderen og mottageren over tilstanden til lagringsområdet, dvs. blokkere senderen når lageret er fullt og blokkere mottageren når lageret er tomt, brukes to Condition-variabler: dataElements og freeSpace. Studer selv hvordan de brukes i get- og put-metodene for å synkronisere tråder over en tilstand, ikke over hvilken programkode som utføres (slik en Lock vil gjøre).

Merk forøvrig at flere tråder kan sende data gjennom put-metoden, selv om det ikke er aktuelt med Pico, som bare kan kjøre to tråder. Setter man ESP32 opp med mer enn to tråder er programmet ikke lenger portabelt til Pico.

import _thread
class FIFO:
    def __init__(self,capacity):
        self.storage = [0 for i in range(capacity)]
        self.CAP = capacity
        self.dataElements = Condition(0)
        self.freeSpace = Condition(capacity)
        self.front = 0
        self.back = 0
        self.mutex = _thread.allocate_lock()
        
    def _in(self,obj): # Put object at back of queue
        with self.mutex:
            self.storage[self.back] = obj
            self.back = (self.back+1) % self.CAP
            
    def _out(self): # Tak object from front of queue
        with self.mutex:
            obj = self.storage[self.front]
            self.front = (self.front+1) % self.CAP
        return obj
    
    def put(self,obj): # Put object in FIFO, block if no space
        self.freeSpace.wait()
        self._in(obj)
        self.dataElements.notify()
        
    def get(self): # Get object from FIFO, block if no data
        self.dataElements.wait()
        obj = self._out()
        self.freeSpace.notify()
        return obj

Legg merke til at self.mutex.acquire og self.mutex.release er nå erstattet med with self.mutex. Bruk av with-statement gjør koden letter å lese, og er forklart f.eks. her.

Et testprogram for FIFO-klassen

Her viser jeg også et kort testprogram som illustrerer hvordan FIFO-klassen kan brukes av flere tråder. Programmet starter med at hovedtråden sender 5 tegnstrenger til FIFO-objektet før den blir blokkert, deretter skriver den tegnstrengene 5-10 i samme takt som undertråden (mottageren) leser strengene 1-5.

# Test-kode

import _thread,time
def subthread(fifo):
    time.sleep(1)
    while True:
        o = fifo.get()
        print(o)
        time.sleep(0.5)

# Now test FIFO
fifo = FIFO(5)
_thread.start_new_thread(subthread,(fifo,))
for i in range(10):
    str = "string nr %d"%i
    fifo.put(str)
    print("Fra hovedtråd: %s"%str)

Sluttord

De nye mikrokontrollerne med flere CPU-kjerner og som kan programmeres i MicroPython representerer en veldig interessant utvikling. Programmering av Internet-of-Things anvendelser trekker fordeler av et godt programmeringsspråk som utnytter maskinvareressursene godt.

Merk at både ESP32 og den versjonen av Pico som kalles Raspberry Pico W, også har en WiFi-modul som lar enheten kople seg til eksisterende trådløse nettverk, eller de kan sette opp sitt eget nettverk som andre maskiner kan kople seg til.

Og jeg håper at Condition- og FIFO-klassen som jeg har laget til dette innlegget kommer til nytte. Det er lettere å lage stabile og korrekte flertrådsprogrammer med hjelpeklasser som støtter en god programmeringsmodell, i dette tilfellet produsent-konsument-modellen.

Docker – nettverk og svermer

 220 total views,  1 views today

Anders Fongen, september 2022

Dette er et blogginnlegg som skal diskutere nettverk av Docker-kontainere, hvordan de kan kommunisere gjennom beskyttede interne nett, og hvordan de kan opptre i svermer under styring av en sentral kontrollnode. Forutsetningen for å få godt ubytte av denne teksten er grunnleggende kjennskap til bygging og kjøring av vanlige Docker-komponenter (eng. images)

Vi kan enkelt tenke oss fornuftige anvendelser hvor en Docker-komponent har nytte av å påkalle tjenester fra en annen komponent. Dette er da typisk en komponent som ikke betjenes med en HTML-basert web -dialog, men som benytter maskin-til-maskin kommunikasjon (m2m). Slik kommunikasjon kan gjerne være basert på HTTP-protokoll og benytte programmeringsbiblioteker for dette, men vil ha innholdet kodet med XML eller JSON. Slike komponenter forbinder vi med begrepet Service Oriented Architecture (SOA). Jeg kommer ikke til å komme inn på slik programmering her, men det finnes mengder av læremidler der ute basert på ditt favoritt programmeringsspråk.

Beskyttet nettverk internt i Docker

Når en komponent ønsker å kalle en annen, er det selvfølgelig mulig å utplassere den kalte komponenten slik at den kan kalles fra “utsiden” (evt. hele Internet) og bruke denne utvendige IP-adressen som servicepunkt. Det er lett å tenke seg hvorfor dette ofte ikke er ønskelig: Uvedkommende kan fritt utnytte og sabotere tjenesten med mindre man lager mekanismer for adgangskontroll, noe som strengt tatt ikke burde vært nødvendig.

Docker tilbyr derimot “interne” nettverk som kun er synlig for Docker-kontainere på den samme maskinen og som er koplet til det interne nettverket. I det man skaper et slikt nettverk:
$ docker network create my-internal-network
$ docker network ls
$ docker network inspect my-internal-network

vil det også bli tildelt en subnett-adresse, og forsynes med tjenester for tildeling av adresser fra subnettet (lik DHCP) og tjenester for å finne IP-adresser til de tilkoplede kontainerne (lik DNS). For siden å kople en kontainer til nettverket kan vi velge to metoder:

  1. Kople til nettverket i det kontaineren skapes, f.eks slik:
    $ docker run -p 80:80 --name mycontainer --net my-internal-network image-name

    Med denne metoden vil kontaineren mycontainer være knyttet til kun my-internal-network. Om kontaineren skal være knyttet til flere nettverk, bruk metode nr.2:
  2. Lage kontaineren, kople til nettverket og starte programmet, i tre steg (eksempelvis)
    $ docker create -p 80:80 -name mycontainer image-name
    $ docker network connect my-internal-network mycontainer
    $ docker network connect my-second-internal-network mycontainer
    $ docker network disconnect bridge mycontainer
    $ docker start mycontainer

    Denne listen av kommandoen viser bruken av create, som oppretter en kontainer uten å starte den, network connect som kopler kontaineren til interne nettverk, og disconnect, som kopler fra nettverk. Deretter starter kjøringen av programmet i kontaineren med $docker run.

Det forholder seg nemlig slik at en kontainer som standardinnstilling koples til et nettverk kalt bridge. Alle andre kontainere vil også ha forbindelse til denne, noe som i mange tilfeller ikke er ønskelig. Metode nr.1 vil erstatte bridge-forbindelsen med en forbindelse til my-internal-network. Metode nr.2 vil legge nye forbindelser til eksisterende.

Eksempel 1 – BusyBox

Som eksempel på viste metode nr.2 kan komponenten BusyBox brukes. Den gir et lite Linux-kjøremiljø som vi kan skrive kommandoer til.

Først lister vi opp eksisterende nettverk og skaper to nye:


Vi er interessert i å vite subnet-adressene til disse to nye nettene:


Vi skaper en kontainer med BusyBox-komponenten og knytter den til de to nye nettverkene, i tillegg til bridge:


Nå starter vi kontaineren bb med BusyBox inni, og skriver kommandoen ip a for å se nettverkskonfigurasjonen på komponenten:

Fra denne kommandoen ser vi de tre ethernet-adaptrene eth0, eth1 og eth2 med subnettadresser som tilsvarer nettverkene bridge, my-net-1 og my-net-2. Som nevnt tidligere ville vi trolig koplet fra bridge for å unngå påvirkning fra uvedkommende komponenter.

Eksempel 2 – Trafikk mellom to Docker-komponenter

Det første eksemplet viste kun konfigurasjon av nettverksforbindelser, ikke hvordan vi kan bruke dem. Derfor vil vi nå sette opp komponenten first slik at vi kan kalle den fra busybox med linux-kommandoen wget og vise html-innholdet som first sender. Komponenten first er beskrevet i et tidligere blogginnlegg.


Her brukes kommandoene som er vist som metode nr.1 ovenfor og vi starter henholdsvis first og busybox med forbindelser til my-net-3. I busybox bruker vi kommandoene slik:

  1. ip a for å se at vi har fått et nettverksadapter med en adresse fra subnettet til my-net-3
  2. ping first for å vise at det foreligger en navnetjeneste som kopler navnet first (navnet på kontaineren) til ip-adressen 172.27.0.2, og at det foreligger en virkelig forbindelse dit.
  3. wget first:8080 for å sende et http-forespørsel til 172.27.0.2, port 8080 og lagre svaret på filen index.html.
  4. more index.html for å vise innholdet av denne filen på konsollet.

Vi har altså med dette eksemplet demonstrert at to komponenter kan ha en nettverksforbindelse som er skjermet fra andre komponenter. Det som er verd å huske er:

  • Interne nettverk får automatisk en subnett-adresse.
  • Containere som kopler seg til et internt nettverk får tildelt IP-adresse automatisk.
  • Interne nettverk (unntatt bridge) har en navnetjeneste som returnerer IP-adressen til navngitte containere. Derfor er det lurt å gi containere et navn med --name parameteren.

Av en eller annen grunn er det ingen navntjeneste i nettverket bridge, slik at ping first ikke vil fungere dersom komponentene ønsket å kommunisere over det nettverket.

Docker-compose

En applikasjon vil gjerne bestå av flere Docker-containere som samarbeider og må konfigureres og startes på en kontrollert måte. Enkeltkommandoer som vist i eksemplene ovenfor kan kanskje settes sammen i en kommandofil (.bat, .sh) og kjøres samlet. Dette forutsetter at du har konsoll-adgang til maskinen som skal kjøre applikasjonen, og at ulike maskiner kan forstå denne kommandofilen. Dette er forutsetninger som ikke kan garanteres. Docker-compose vil øke portabiliteten av applikasjoner og forenkle konfigurasjonen, siden alt foregår i én fil.

Derfor kommer Docker-compose inn som et verktøy hvor detaljer vedrørende konfigurasjon og kjøring for alle komponentene i applikasjon kan uttrykkes i én fil. Denne filen har en såkalt YAML-syntaks, som skal vises i eksemplet som følger.

Filen skal hete docker-compose.yml. Som eksempel på utforming skal vi bruke konfigurasjonen i eksempel nr.2 ovenfor. Innholdet i filen er som vist under:

Syntaksen og mulige informasjonselementer i denne YAML-filer (uttales “jammel”) er ganske omfattende, og en oversikt finner du her. Derfor velger jeg heller å demonstrere ett bestemt kjent tilfelle med en typisk struktur: nettverksforbindelse, volumer, eksponerte porter. Elementene under disse to vil være tilsvarende de parametrene vi tidligere ga på kommandolinjen. For first, legg merke til at build: nå må ha et stinavn til der hvor kildefilene til first ligger.

Syntaksen i den viste docker-compose.yml skal forstås som et hierarki der innrykk av teksten viser en “tilhørighet” til linjen ovenfor med kortere innrykk. Her har vi altså tre hovedkategorier: services, volumes, networks. Før en kontainer kan knyttes til et volum eller nettverk må disse først deklareres på denne måten. Under networks finner vi hvilket nett som skal brukes i applikasjonen og hva dette skal hete.

Under services finner vi de to vi har arbeidet med så langt, bb og firsts. For bb kommer det to linjer knyttet til stdin_open og tty, som har å gjøre med at vi ønsker konsolltilgang til denne kontaineren, noe som i mange tilfeller ikke vil være aktuelt. Derfor vil denne viste filen ha de fleste av de egenskapene du i praksis vil trenge.

La oss nå gi denne filen til Docker-compose og se hva som kommer ut av det. Arbeidskatalogen (current directory) må være den som inneholder docker-compose.yml . Oppstart skjer med kommandoen $ docker-compose up:


Nå er både bb og first started i hver sine kontainere, men vi får ingen konsolltilgang til å skrive kommandoer i. Til forskjell fra eksempel nr.2 må vi skrive dette i tilegg:
$ docker exec -it bb sh
Denne kommandoen utfører kommandoen sh i kontaineren som heter bb. Parameteren -it gjør at sh også kommuniserer med konsollet, så vi får en interaktivt Linux-konsoll. Nå kan vi teste at det er kommunikasjon mellom bb og first:


Dette må dog skrives i et annet CMD-vindu, fordi vinduet der vi startet docker-compose er opptatt. Der kan vi derimot lukke applikasjonen og stanse kontainerne ved å trykke ctrl-C noen ganger:


Kommandoen $ docker ps -a viser en liste over kontainerne og at disse er stoppet. Vi kan nå slette én og en kontainer med kommandoen $ docker rm bb first, men en enklere måte er $ docker container prune, som sletter alle inaktive kontainere under ett. Inaktive kontainere kan okkupere en del ressurser i systemet, og det er sunt å rydde regelmessig.

Docker swarms

I et storskala informasjonssystem vil en web-tjeneste kjøre på mange maskiner i parallell. Da oppnår man at maskinene kan fordele kundetrafikken mellom seg for å oppnå høyere total ytelse (kalt lastfordeling), og at noen av maskinene kan falle ut av drift uten at systemet som sådan blir utilgjengelig (kalt fail-over). Denne formen for ressursorganisering krever en sjef (manager), som fordeler forespørsler mellom arbeiderne (workers) og som holder oversikt ettersom maskiner går inn eller ut av drift.

Docker-arkitekturen egner seg godt til å inngå i en slik storskala arkitektur, siden tilstandsløse komponenter kan dupliseres på flere maskiner og utføre nøyaktig den samme tjenesten. Kunder vil se ett eneste servicepunkt (IP-adresse og port) og ikke merke noe til at tjenesten er fordelt på mange maskiner. La oss derfor se på hvordan Docker-komponenter kan organiseres som en Docker swarm:

Bruk helst Linux-maskiner

Selv om kommandoene for Docker-svermer også finnes på Windows-versjonen av Docker-systemet, er det en del egenskaper som simpelthen ikke virker som forventet. Det er derfor å anbefale at et eksperiment med Docker-svermer kun benytter Linux-maskiner. Husk også at Docker-komponenter ikke er arkitekturnøytrale, de kan ikke kjøre både på X86- og ARM-maskintyper. Ikke blande f.eks. Raspberry Pi (ARM) med ordinære PCer (X86) i en Docker-sverm.

Alt foregår fra sjefen

For å bygge opp en sverm må maskinene kunne nå hverandre gjennom et IP-nettverk, det er ikke tilstrekkelig at sjefen kan nå alle arbeiderne, arbeiderne må også kunne kommunisere med hverandre. For først å bygge opp en sverm gjør vi følgende:

  1. Bestem hvilke maskiner som skal delta, skriv ned deres IP-adresser og lag et kart som viser hvem som er sjef og hvem som er arbeidere. Fra konsollet til sjefen, logg inn (med ssh) på alle arbeiderne i separate konsollvinduer. Alle kommandoer som vises som eksempler må kjøres i sudo-modus (skriv sudo bash for komme dit).
  2. På alle maskinene i svermen, sørg nå for at alle kontainere er stanset ($ docker stop ..., $ docker rm ...) og kontroller resultatet med $ docker ps -a.
  3. På sjefens maskin, sjekk om det allerede er en sverm med $ docker node ls. Dersom det finnes en, fjerne dem med kommandoen $ docker swarm leave --force.
  4. På sjefens maskin, opprett en sverm med kommandoen $ docker swarm init. Reponsen kan se omtrent slik ut:
    Swarm initialized: current node (icv8b9vnemxfmym4lb1gbto7f) is now a manager
    To add a worker to this swarm, run the following command:
    docker swarm join --token SWMTKN-1-0ezzbg9iw2glu6d7xd6c2shjvhadr7zlemwuf4l9besxl2iogl-a1yngnqehgyub0phopuslysly 192.168.2.116:2377
  5. Ta en kopi av den uthevede teksten, og lim den inn i konsollvinduet til alle arbeiderne slik at kommandoen utføres der. De vil nå kople seg til sjefen (til ip-adressen som er vist i kommandoen) og slutte seg til svermen. Nå vil de etterhvert få opprettet kontainere for applikasjonstjenester og motta klientforespørsler.
  6. Fra sjefens konsoll skriv $docker node ls for å kontrollere at alle arbeiderne er kommet inn i svermen.

Utplassering av en tjeneste i svermen

Nå kan sjefen plassere en tjenesten inn i svermen. Dette betyr, som tidligere nevnt, at en applikasjon blir plassert ut på én eller flere av arbeiderne (inkludert sjefen), og en forespørsel fra en klient blir betjent av én av dem.

Hvilken IP-adresse leder inn til den utplassert tjenesten? Svaret er at alle IP-adressene i svermen (altså endepunktet til alle arbeiderne og sjefen) gir adgang til tjenesten, også de arbeiderne som ikke betjener selve tjenesten. Det eksisterer et eget “overlay” nettverk mellom maskinene som fremforhandler fordelingen av arbeidsoppgaver og som videresender forespørsler fra klienter til en kandidat-arbeider.

Kommandoen skal skrives på sjefens konsoll. Sånn kan den se ut:

$ docker service create --name first --replicas 3 --publish 8080:8080 andfon7a/first

Den eneste nye parameteren siden de tidligere eksperimentene er --replicas som angir det maksimale antall arbeidere (inkl. sjefen) som skal laste denne applikasjonen. Legg også merke til at Docker-komponenten ikke må ligge på sjefens egen maskin, den kan hentes fra Docker-hub om nødvendig (og den må da evt. plasseres der på forhånd med $ docker push andfon7a/first).

Med kommandoen $ netstat -ant på alle maskinene i svermen kan man nå konstatere at port 8080 er “åpen”. Med en web-leser kan man kontake én av maskinenes IP-adresse på port 8080 og konstatere at tjenesten utføres korrekt.

For å inspisere tjenesten bruk kommandoen $ docker service inspect --pretty first. For å fjerne den skrives $ docker service rm first.

Opp- og nedskalering: Det opplagt nyttige i svermen er muligheten for å endre kapasiteten ved å øke eller redusere antall maskiner som deltar i en tjeneste. Det gjøres som følger (i dette eksemplet reduseres antallet fra 3 til 2):

$ docker service scale first=2

Konklusjon

Målet med dette blogginnlegget var å gi en kortfattet, men ukomplett, innføring i hvordan man setter opp nettverk og svermer av Docker-komponenter. Det fulle omfanget av muligheter og detaljer er mye større, men er blitt utelatt fordi det er lagt vekt på å vise konkret hvor enkelt det er å sette opp en grunnleggende tjeneste.

Jeg anbefaler først leseren å gjennomføre disse eksemplene på eget utstyr og så gjøre nødvendige endringer etter egen interesse: F.eks. hvordan en sverm kan operere med permanent lagring gjennom en filtjener eller en SQL-database. Slike komponenter er ikke enkle å replikere i en sverm og vil naturlig ligge i egne tjenester, gjerne i frittstående Docker-kontainere med muligheter for å lagre data i volumes.

Utplassering av Docker-komponenter i Microsoft Azure

 472 total views

Anders Fongen, september 2022

Å konstruere Docker-komponenter og utplassere (eng. deploy) dem i en lokal kontainer på egen maskin er en ganske overkommelig oppgave. Mer omstendelig er det å sette slike komponenter i drift i en sky. De store skytjenestene (f.eks. Amazon AWS, Google Cloud og Microsoft Azure) er svært store og komplekse installasjoner som skal kunne betjene kunder med helt ulike behov. De tilbyr alle en web-tjeneste (portal) som lar kunden laste opp egenutviklet programvare og konfigurere samspillet med de tjenestene som skyen tilbyr (f.eks. sikkerhet, backup, navnekataloger, fillagring og SQL-databaser). For alle disse skyene blir denne portalen svært omfattende og omstendelig å bruke. Kvaliteten på dokumentasjonen er dessuten svært varierende.

Jeg har tatt for meg utvikling, utplassering, feilsøking og drift av Docker-komponenter med påfølgende utplassering i Microsoft Azure. Etter vellykket utplassering er tjenesten tilgjengelig på Internet. Dette blogginnlegget vil vise hvordan det kan skje.

Hvorfor er skyen en god idé?

Visst er det mulig å sette opp en webtjener på ditt eget hjemmenett, bestille fast IP-adresse hos Internet-leverandøren, sette opp port forwarding på hjemmeruteren, og gi hjemmeadressen et DNS-navn. Denne prosessen har jeg forøvrig beskrevet i et eget blogginnlegg.

Med et slik løsning må du selv ta hånd om sikkerhet, backup, reserveløsninger ved systembrudd, brukertillatelser, og fremfor alt skalering. En vellykket tjeneste på Internet kan oppnå en svært rask vekst i kundemengden, og dersom du ikke klarer å opprettholde en akseptabel responstid under slike forhold har du mislykkes.

Disse kravene kan skyen innfri, fordi de besitter store datasentraler med høy nettverkskapasitet, og de kan raskt øke kapasiteten på tjenesten din, de kan endog gjøre det automatisk. Personellet i datasentralene er eksperter på skalering, sikkerhet, overvåking og etterforskning, og de har ingen annen jobb enn å passe på at tjenestene leverer stabilt og raskt. Du har andre oppgaver og vil ikke klare å gjøre denne jobben like godt.

Docker

Programvare som skal kjøres som en skytjeneste må skrives etter bestemte regler, og utplasseringen trenger noen opplysninger som må fremskaffes av kunden. Docker er programvare for å lage og kjøre skytjenester. Jeg skal ikke forklare Docker grundig her, fordi det finnes mye informasjon om nettopp dette på nettet:

Det er nødvendig å gå gjennom noen eksempler på bygging og kjøring av Docker-komponenter på lokal maskin: Når Docker-komponenter kjøres på lokal maskin, f.eks. BusyBox, kan de kommunisere med brukeren gjennom konsollet, dvs. skjermen som brukes for å bygge og starte komponenter. Dette er ikke mulig på Azure, hvor komponenten må kommunisere med omverdenen gjennom nettverket (Internet).

Nettverkstrafikk kan omfatte protokoller som ssh, http, mqtt og det meste annet, men komponenten må selv besørge selve protokollen, Azure besørger bare UDP- og TCP porter, samt en brannvegg for å beskytte mot angrep via nettverket. Vi skal demonstrere bruk av Azure med web-komponenter som benytter http-protokoll.

Installasjon av programvare

For å kommunisere med Azure trengs programvaren Docker Desktop, installert på Windows eller Mac (bruk lenken ovenfor). I CMD-konsollet (Windows) kan man nå skrive $ docker for å utføre diverse kommandoer.

I den påfølgende tekst vil jeg bruke “$”-symbolet for å vise en kommando slik den skal skrives inn i CMD-konsollet. Dollartegnet skal altså ikke skrives inn.

For å utplassere Docker-komponenter på Azure trenger du følgende brukerkontoer:

  1. Hos hub.docker.com. Her plasserer du dine komponenter slik at andre kan laste dem ned til seg og bruke dem. Kontoen er gratis.
  2. Hos portal.azure.com. Her foregår selve kontrollen av Azur-aktivitetene. Duu må registrere et betalingskort, men du får også en romslig kvote gratis som du kan eksperimentere med.

Eksperimentprogrammer

To små Python-programmer blir brukt i denne demonstrasjonen: De ene er et enkelt tilstandsløst web-program som adderer to tall og viser resultatet, det andre web-programmet benytter en tekstfil som lagres på et permanent lager. Skillet mellom disse to programmene er hvordan de bruker permanent lager. Programmene ser slik ut:

Summere to tall (tilstandsløst web-program)

first.py:
import web
urls = ('/','index')

class index:
   def GET(self):
      f = open("first.html","r")
      return f.read()
   def POST(self):
      i = web.input()
      if i.a.isnumeric() and i.b.isnumeric():
         return "Svaret er %d" % (int(i.a)+int(i.b))
      else:
         return "Kun numerisk inndata er lovlig"

if __name__ == '__main__':
   app = web.application(urls,globals())
   app.run()

first.html:
<html><body>
  <h1>Addere to heltall</h1>
  <form method='POST'>
     <input name='a', size='4'>
     <input name='b', size='4'>
     <input type='submit' value='finn sum'>
   </form>
</body></html>

Dockerfile:
FROM python:3.8
WORKDIR /usr/src/app
COPY . .
RUN pip3 install web.py
EXPOSE 8080
CMD ["python","./first.py"]

Dette webprogrammet kan startes direkte fra kommandolinjen og tilby en enkel tilstandsløs tjeneste. Vi kan når som helst restarte tjenesten uten å forstyrre klientene. Vi kan lage en Docker-komponent og kjøre den lokalt på vår egen maskin med disse kommandoene (de tre filene må ligge på samme katalog, og katalogen må være arbeidskatalog (current directory):

$ docker build -t first .
$ docker run --name container1 -d -p 8080:8080 first

Nå er denne webtjenesten tilgjengelig på http://localhost:8080/. For å stoppe og fjerne tjenesten (f.eks. før utplassering av en ny programversjon) kan disse kommandoene skrives:

$ docker ps -a
$ docker stop container1
$ docker rm container1

Dele en liten datafil (tilstandsfylt web-program)

third.py:
import web, os

urls = ('/','index')
os.environ['PORT'] = '80'
app = web.application(urls, locals())
filename = "message.txt"
class index:
   def GET(self):
      if not os.path.isfile(filename):
         f = open(filename,"w")
         f.close()
         message = "Ingen melding"
      else:
         f = open(filename,"r")
         message = f.read()
      resp ="<html><body><h2>Din siste melding var: %s"%(message)
      resp += "<p>Skriv inn melding:<form method='POST'><input name='m' size='20'>"
      resp += "<input type='submit' value='Lagre melding'></form></body></html>"
      return resp
   def POST(self):
      i = web.input()
      f = open(filename,"w")
      f.write(i.m)
      f.close()
      raise web.seeother('/')

if __name__ == '__main__':
   app.run()

Dockerfile:
FROM python:3.8
WORKDIR /usr/src/app
COPY . .
RUN pip3 install web.py
EXPOSE 80
CMD ["python","./third.py"]

Dette web-programmet vil ta imot en tekstlinje og lagre den på en fil, som siden kan hentes frem av denne eller andre klienter. En form for datadeling altså. Filen som lagrer tekstlinjen bør bevares også når programmet stopper og starter igjen. Dersom vi kjører third.py som et vanlig Python-program vil filen ligge på vertsmaskinens filsystem, og vil dermed bevares slik vi ønsker.

Dersom vi lager en Docker-komponent som med forrige eksempel, vil også denne tekstlinjen bevares og utveksles mens Docker-komponenten kjører. Dersom Docker-kontaineren stoppes og startes igjen:

$ docker stop container1
$ docker start container1

(gitt at kontaineren er gitt navnet container1 ved docker run – kommandoen), da vil fortsatt tekstlinjen bevares slik vi ønsker. Dersom vi derimot fjerner kontaineren og lager en ny:

$ docker stop container1
$ docker rm container1
$ docker run --name container1 -d -p 80:80 third

Da har tekstlinjen fått det innholdet den hadde da Docker-komponenten ble laget (evt. tom), og endringer etter det tidspunktet er gått tapt. Altså, ikke slik vi ønsker at webtjenesten vår skal fungere.

Docker-komponenter med volumes

For å kunne bevare data også når docker-kontaineren slettes og skapes på nytt, må dataene lagres utenfor kontaineren, i vertsmaskinens filsystem. Dette er mulig å få til gjennom å montere en del av vertsmaskinens filsystem inne i kontainerens. Vi skal først endre én programsetning i third.py:

filename = "/app/message.txt"

og bygger web-tjenesten på lignende måte:

$ docker build -t third .
$ docker run --name container3 -v textdata:/app -d -p 80:80 third

Da oppnår vi nemlig det vi ønsker, nemlig at innholdet på området textdata i vertskapets filområde (i et nærmere bestemt filområde inne i Docker-installasjonen) vises under katalogen /app inne i kontaineren, og data som lagres under /app blir bevart selv når kontaineren slettes og gjenskapes. Dette kan kontrolleres ganske enkelt i et eksperiment.

Merk at dette permanente lageret kan lagre alle slags objekter, ikke bare tekstfiler: Bilder, lyd og selvfølgelig en databasefil fra f.eks. SQLite.

Litt om tilstander i Azure/Docker

Azure, i likhet med andre skyleverandører, lar Docker-komponenter kjøre i omgivelser som støtter stor skala, høy sikkerhet og dynamisk konfigurasjon. Det vil si at web-tjenesten som kjører kan gis et elastisk ressurstilbud ettersom pågangen øker og minker. Slik automatisk skalering skjer gjennom at Docker-komponenter kjører samtidig på mange maskiner i parallell (kalt flere instanser) og fordeler forespørselene mellom seg, og at antallet samtidige maskiner øker og minsker dynamisk.

Med datalagring inne i komponenten, slik vi har demonstrert med third.py, vil ikke en slik skalering kunne finne sted, fordi alle instansene vil ha ulikt datainnhold siden de betjener ulike forespørsler. Nei, skalering krever at Docker-komponenten benytter en separat lagringstjeneste som behandler skrivbare data (som filen message.txt i third.py ovenfor). Kun lesbare data kan fortsatt legges inne i Docker-komponenten dersom mengden ikke er for stor.

Dette skillet mellom brukerbetjening, forretningslogikk og datalager er en populær og velprøvd konstruksjonsteknikk for store informasjonssystemer. Trafikken mot datalageret (kalt back-end) er ofte lavere enn mot front-end (web-tjenesten) og datalageret kan skaleres med andre teknikker enn en front-end. Derfor ser vi at dette skillet mellom utføring og lagring i adskilte tjenester går igjen hos alle skyleverandører.

Utplassering av third.py i Azure

Vi skal hoppe over utplassering av first.py i Azure fordi det skjer på samme måten som med lokal Docker-plattform med noen små endringer, som vil fremgå av forklaringen som følger. Jeg skal gå gjennom utplassering av third.py i Azure, etter at den ene programsetningen er endret (filename = "/app/message.txt). Trinn for trinn er prosessen denne:

  1. Bygge en lokal Docker-komponent. Navnet på komponenten må prefikses med brukernavnet i Docker-hub, som i mitt tilfelle er andfon7a:
    $ docker build -t andfon7a/third .
  2. Plassere komponenten i Docker-hub. Det er nemlig herfra at Azure skal hente den senere
    $ docker login (Her kreves brukernavn og passord til brukerkontoen din)
    $ docker push andfon7a/third
    $ docker logout
  3. Logge inn i Azure og opprette filområdet
    $ docker login azure (Får du feilmelding “Azure lookup failure”, har du feil
    versjon av Docker installert. Bruk nyeste versjon av Docker desktop)
    $ docker context create aci mycontext (velg “create new resource group”)
    $ docker context use mycontext
    $ docker volume create mydata --storage-account andfon7astorage
  4. Lag en kontainer og start komponenten i den. Den lastes inn fra Docker-hub
    $ docker run -p 80:80 --name third --domainname andfon7a -d -v andfon7astorage/mydata:/app andfon7a/third
  5. Sjekk at kontaineren kjører og hvilket DNS-navn den har fått.
    $ docker ps -a
    – Sjekk nå med en web-leser at du får tak i tjenesten med dette DNS-navnet

Microsofts egen infoside om denne prosessen finner du her. Docker sin tilsvarende veiledning finnes her.

For å stoppe/starte/fjerne kontaineren bruker du kommandoene:
$ docker stop third
$ docker start third
$ docker rm third

Starting av en kontainer kan ta lengre tid enn kun kommandoen i konsollet. Bruk $docker ps -a for å se status i oppstartsarbeidet.

Om DNS-registreringen: For bruk av --domainname parameteren gjelder det at DNS-navnet blir registrert på den tildelte IP-adressen med en TTL-verdi på 300 sekunder. Dersom du under f.eks. eksperimentarbeid statid starter containeren på nytt kan du komme i situasjonen hvor DNS-tjenesten fortsatt en stund returnerer den forrige IP-adressen. Om du utelater --domainname parameteren vil $docker ps vise deg IP-adressen i den samme kolonnen , som alltid er riktig.

Huskn

Når du vil igjen jobbe lokalt eller mot Docker-hub:
$ docker logout azure
$ docker context use default

Docker-tjenester fra Amazon AWS og Google Cloud

Amazon AWS og Google Cloud er to konkurrerende skytjenester som i det store bildet har mye til felles. Begge disse kan enkelt støtte tilstandsløse Docker-komponenter, enklest gjennom egne kontrollprogrammer (à la docker) som installeres i Windows-konsollet (cmd).

  • Google Cloud betjenes av programmet Gcloud som installeres på din lokale maskin. Opplastingen skjer direkte fra lokal maskin med commandoen gcloud run deploy, og nødvendige tilleggsopplysninger legges inn i den påfølgende dialogen.
  • For Amazon AWS skjer utplasseringen ved hjelp av en tilleggsfil som inneholder de nødvendige ekstraopplysningene (som ikke ligger i Dockerfile). For en veiledning for dette, sjekk avsnittet “Docker on AWS” på denne websiden.

Ingen av disse to skytjenestene støtter derimot Volumer, slik vi har vist er mulig i Azure. Derimot har begge database-tjenester som kan brukes av Docker-komponenter for lagring. Dette dekker noe av behovet for permanent lagring, men krever også at programvaren i større grad er tilpasset for dette og blir derfor mindre portabel enn hva som er ønskelig. Databasetjenester er dessuten krevende å konfigurere, sammenlignet med et filsystem.