Infraštruktúra postavená na kontajneroch

o kontajneroch, o (mikro) službách spustených v kontajneroch, o metodológii The Twelve-Factor App

Video k prednáške

Motivácia

Smart Department: Architektúra

Doteraz sme sa venovali nižším vrstvám, ktoré sú štandardne blízko ku koncovému používateľovi. Dnes sa porozprávame o tom, ako to vyzerá v cloud-e a akým spôsobom tam budeme nasadzovať naše riešenia/služby. Demonštrujeme si to na príklade jednoduchej webovej mikroslužby (tzv. microservice) a porozprávame sa o tom, ako ju pripraviť pre produkčné nasadenie a na čo si dávať pozor.

Mikroslužba

[definition]

Mikroslužba je malá, samostatná časť softvérového systému, ktorá zabezpečuje konkrétnu funkciu.

Jednoduchšie je si vysvetliť čo je vlastne mikroslužba na jej vlastnostiach:

  • robí/vkonáva jednu jasne danú činnosť,
  • je škálovateľná,
  • je nezávislá od iných inštancií seba samej,
  • jej požiadavky na systémové zdroje sú minimálne.

V praxi si môžeme predstaviť aplikačné rozhranie e-shopu, ktoré nepoužíva konvenčný API server ale mikroslužby - v prípade okrajovej alebo nežiaducej situácie v API ktoré zaobstaráva filtrovanie výrobkov na webe môže služba poskytujúca túto službu zlyhať, avšak ostatné mikroslužby ako sú objednávkové rozhrania, platobné brány, newslettery a množstvo iných budú fungovať bez následkov. Takéto riešenie samozrejme prináša aj nevýhodu - tou je zvýšená komplexita samotného riešenia, kedy spravovanie veľkého množstva mikroslužieb vytvára relatívne veľkú sieť v cloud-e. Z pohľadu výkonu by sa však takáto architektúra mala prejaviť len minimálne, keďže vhodne nadizajnovaná mikroslužba spotrebúva zanedbateľné množstvo zdrojov (bežne sú to desatiny virtuálneho jadra CPU a desiatky MB pamäte RAM).

V našom prípade si demonštrujeme fungovanie mikroslužieb pomocou jednoduchého úryvku kódu ktorý vykonáva monitorovanie počasia cez voľne dostupné API.

Prvým krokom je vytvorenie nášho projektu. V ňom vytvoríme priečinok app/ so súborom main.py ktorý bude mať nasledujúci obsah:

import httpx
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get('/api/weather')
def get_weather():
    location = httpx.get(
        'https://geocoding-api.open-meteo.com/v1/search',
        params={'name': 'Košice', 'count': 1},
    ).json()['results'][0]
    response = httpx.get('https://api.open-meteo.com/v1/forecast', params={
        'latitude': location['latitude'],
        'longitude': location['longitude'],
        'current': 'temperature_2m,relative_humidity_2m,apparent_temperature,'
                   'is_day,precipitation,weather_code,cloud_cover,'
                   'pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m',
        'wind_speed_unit': 'ms',
        'timezone': 'auto',
        'forecast_days': 1,
    })
    return response.json()

if __name__ == '__main__':
    uvicorn.run('main:app', host='0.0.0.0', port=8000, reload=True)

Ako ju nasadíme ?

Spustenie priamo v našom operačnom systéme

Bežne sa pracuje priamo s jazykom Python tak, že nainštalujeme jeho balíčky a knižnice na náš počítač, čo by nám umožnilo spustením príkazu python app.py spustiť našu aplikáciu. V tomto prípade je však asi nepraktické dávať naším kolegom, ktorí pracujú na tej istej aplikácii, prístup na náš stroj. Taktiež vzniká aplikácia ktorá funguje “na našom systéme” - práve náš/váš počíťač môže mať nainštalované rôzne aplikácie ktoré ovplyvňujú fungovanie našej služby - či už sa jedná o LogMeIn Hamachi(pamätníci poznajú), aplikáciu na riadenie IP kamery z TEMU od výrobcu s pre nás nečitateľným názvom alebo špeciálna verzia pythonu ktorá je vydaná iba pre náš počítač lebo používame taký procesor s takou architektúrou že sa “nekamaráti s ničím iným”(napr. M1 od Applu mal kedysi takýto problém).

Spustenie na bare-metal serveri

V tomto prípade je riešenie jednoduché - máme server s Windows/Linux operačným systémom a spúšťame kód ktorý sme tam nahrali. Naši kolegovia sa vedia pripájať cez SSH/RDP a taktiež pracovať na tej istej aplikácii. Samotná inštalácia a vyladenie je síce problematická, ale po prvom zostavení to “proste funguje”. Distribúcia kódu je riešená či už sťahovaním alebo Git-om ale nie je práve najpohodlnejšia. V prípade že chceme aplikáciu nasadiť u zákazníka musíme prejsť procesom inštalácie opätovne, čo sa môže skomplikovať ak má zákazník iný model stroja.

Spustenie vo virtuálnom stroji

Tu je už aj distribúcia aplikácie jednoduchšia, keďže virtuálny stroj vieme exportovať a kopírovať medzi kompatibilnými systémami s minimálnym vplyvom hardvéru - o to sa stará hypervízor. Vývoj aplikácie a riešenie chýb je však stále veľmi ťažkopádne. Samotný export virtuálneho stroja je obrovský v porovnaní s veľkosťou našej aplikácie.

Spustenie v kontajneri

Tu sa riešia naše hlavné problémy - distribúcia a vývoj. Každý vývojár si nainštaluje Engine podľa pokynov pre svoj systém. Pre spustenie aplikácie si jednoducho klikne/vyvolá súbor a engine sa postará o to aby dosiahol požadovaný stav. Aplikácia už nie je dodávaná ako obrovský súbor ale ako jeden súbor spustiteľný s použitím Engine-u. Takýto skok sa ťažko zhŕňa v 2 vetách, takže si poďme spoločne prejsť čo to spôsobuje a obnáša …

Porovnanie virtuálnych strojov a kontajnerov

S virtuálnymi strojmi sme sa už stretli. Poďme sa pozrieť na porovnanie servera s virtuálnymi strojmi a kontajnermi:

Porovnanie virtuálnych strojov a kontajnerov

Docker ? Podman ? Engine ?

Tieto slová ste pravdepodobne už aj započuli, ale bez kontextu možno neboli jasné.

[definition]

Kontajnerové engine-y sú programy ktoré vytvárajú, spúšťajú a spravujú kontajnery.

Táto definícia nám teda objasňuje o čom “som točil” vyššie. Docker a Podman sú teda kontajnerové runtime-y - ich účel je rovnaký, len to robia inak a maju mierne odlišné funkcie a samozrejme vývojárov. V priebehu nášho predmetu budeme používať Docker, ktorý je vhodný pre profesionálov ale aj pre začiatočníkov + je dodávaný s pekným grafickým rozhraním pre lokálny vývoj na vašich počítačoch :).

Grafické rozhranie aplikácie Docker Desktop

Vďaka týmto vlastnostiam je pre nás ideálny na vývoj, spúšťanie a simulovanie služieb.

[definition] Oficiálny popis Docker-u na webe projektu

Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker’s methodologies for shipping, testing, and deploying code, you can significantly reduce the delay between writing code and running it in production.

Docker využíva klient-server architektúru, v ktorej používateľ - ako klient - komunikuje s už spomenutým Docker Engine-om. Celková architektúra je znázornená na nasledujúcom obrázku:

Architektúra Docker-u z Blog-u

Čo je to kontajner?

[definition]

Kontajner je samostatná inštancia obrazu softvéru.

Pre jednoduchosť sa zatiaľ môžeme na obraz pozerať ako na zip balík, v ktorom je zabalená aplikácia. Spustením obrazu vznikne kontajner, pričom z jedného obrazu je možné spustiť viacero kontajnerov. Takýto kontajner je spustený ako proces nad jadrom lokálneho operačného systému, teda neobsahuje operačný systém ako taký.

Čo je to obraz?

[definition]

Obraz je pripravený balík ktorý obsahuje aplikáciu, jej potrebné súbory a pokyny na jej spustenie. Spustením obrazu vznikne kontajner.

Ak by sme hľadali nejakú paralelu, tak sa môžeme pozrieť na súbory uložené na disku:

  • (spustiteľný) súbor, ktorý je uložený na disku, reprezentuje obraz,
  • súbor je nemenný (je určený na čítanie),
  • spustením súboru vzniká proces, ktorý reprezentuje kontajner,
  • z jedného súboru viem spustiť ľubovoľný počet procesov,
  • každý spustený proces je jednoznačne identifikovaný svojim PID podobne, ako má každý kontajner svoj vlastný identifikátor kontajnera.

Vytvorenie obrazu pre aplikáciu

Vo svete kontajnerov je vytvorenie jednoduchého obrazu aplikácie možné za pomoci niekoľkých riadkov relatívne “ľudského” kódu. Tieto textové definície sa v prípade Docker-u ukladajú do takzvaných Dockerfile-ov. V našom prípade pre našu aplikáciu bude takýto súbor vyzerať nasledujúco:

# Zaklady obraz na ktorm ideme "stavat" nasu aplikaciu - cerpame z internetu alebo lokalne
FROM python:3.13-slim

# Spustime prikaz ktory potrebujeme - v tomto pripade instalacia balickov Pythonu
RUN pip install --no-cache-dir httpx fastapi[standard] python-dotenv

# Prekopirujeme kod z nasho adresara /app do adresara /app v kontajneri
COPY app/ /app

# Definujeme ze chceme zverejnit port 8000
EXPOSE 8000

# Definujeme kde sa ma nachadzat pracovny adresar nasej aplikacie - ako keby sme pri starte urobili `cd /app`
WORKDIR /app

# Definujeme spustaci prikaz nasej aplikacie
#CMD [ "fastapi", "dev", "main.py" ]
CMD [ "uvicorn", "main:app", "--reload", "--host=0.0.0.0" ]

Ak máme nainštalovaný docker, tak obraz vieme zostaviť jednoduchým príkazom v konzole:

docker image build --tag pocko .

“Best Practices” tvorby SAAS softvéru - The Twelve-Factor app

The Twelve-FactorApp

The Twelve-Factor App je methodológia pre tvorbu aplikácií typu softvér ako služba (z angl. software-as-a-service alebo skrátene SAAS). Pozostáva z nasledujúcich kľúčových pravidiel:

  • Use declarative formats for setup automation, to minimize time and cost for new developers joining the project,
  • Have a clean contract with the underlying operating system, offering maximum portability between execution environments,
  • Are suitable for deployment on modern cloud platforms, obviating the need for servers and systems administration,
  • Minimize divergence between development and production, enabling continuous deployment for maximum agility,
  • And can scale up without significant changes to tooling, architecture, or development practices.

Konfigurácia pomocou premenných prostredia

Služba open-meteo.com narozdiel od iných služieb na nekomerčné účely nevyžaduje API token. Niektoré služby takýto token vyžadujú - v takom prípade je dôležité aby sa s tokenmi narábalo bezpečne a nenechávali sa v kóde “len tak” !

Aktuálne je súčasťou kódu našej aplikácie aj .env súbor s konfiguráciou pre náš dopyt na open-meteo.com. Tento prístup je porušením metodológie The Twelve-Factor App, ktorá vyžaduje striktné oddelenie konfigurácie od kódu aplikácie. Odporúčaním metodológie je miesto toho umiestniť konfiguráciu do tzv. premenných prostredia.

Premenné prostredia

Premenné prostredia (z angl. environment variables) sú premenné nastavené používateľom v operačnom systéme, pomocou ktorých je možné ovplyvňovať správanie spustených aplikácií.

Premenné prostredia sú nastavené mimo zdrojového kódu aplikácie a aj mimo konfiguračných súborov aplikácie. Používajú sa na nastavenie tých hodnôt, ktoré sa menia medzi nasadeniami aplikácie, ako napríklad:

  • umiestnenie databázového serveru a prístup k nemu,
  • prihlasovacie údaje a umiestnenie vzdialenej služby, od ktorej závisí tá naša,
  • port, na ktorom bude aplikácia spustená.

Operačný systém samotný obsahuje množstvo premenných prostredia. Ich zoznam si viete zobraziť príkazom printenv. Od ostatných premenných ich odlíšite najmä tým, že používajú tzv. SCREAMING_SNAKE_CASE notáciu pre pomenovávanie premenných.

Rozličné konvencie pre pomenovávanie

Ak však budete chcieť premenné prostredia vytvoriť z príkazového riadku systému Linux, musíte ich exportovať, a to napríklad takto:

export DB_URI=postgresql+psycopg2://scott:tiger@localhost:5432/mydatabase

Premenné prostredia vo forme konfigurácie aplikácie sa často ukladajú do súboru .env alebo do súborov s príponou .env, ako napríklad local.env. Tento súbor obsahuje zoznam premenných spolu s ich hodnotami vo formáte kľúč=hodnota - nedávno sme na prednáške spomínali key-value pairs. Teraz sa s nimi stretávame a môžu vyzerať napríklad takto:

DB_HOST=localhost
DB_USER=scott
DB_PASSWORD=tiger
DB_NAME=mydatabase

Kolízia názvov súborov .env sa rieši prefixom ako napríklad develop.env.

Konfigurácia aplikácie pomocou premenných prostredia

V jazyku Python na prácu s premennými môžeme použiť priamo balík os a v ňom slovník environ. Obsahuje premenné prostredia pre spustenú aplikáciu.

Miesto toho však použijeme balík pydantic-settings, ktorý je rozšírením modulu pydantic a pomocou neho je možné vytvárať modely pre potreby nastavenia. Výhodou je, že automaticky model zvaliduje a dokáže rovno načítať údaje zo súboru .env, ak sa taký v priečinku, z ktorého sa aplikácia spúšťa, nachádza.

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    location: str
    temperature_unit: str
    model_config = SettingsConfigDict(env_prefix='POCKO_')
    
@cache
def get_settings():
    return Settings()

Pričom bude čerpať z nášho súboru .env:

# Nazov mesta (mozeme doplnit aj krajinu) a jednotka teploty.
POCKO_LOCATION=Košice,Slovakia
POCKO_TEMPERATURE_UNIT=celsius

Následne upravíme hlavný kod tak, aby konfiguráciu použil:

@app.get('/api/weather/{query}')
def get_weather(query: str | None = None):
    settings = get_settings()
    location = httpx.get(
        'https://geocoding-api.open-meteo.com/v1/search',
        params={'name': query or settings.location, 'count': 1},
    ).json()['results'][0]

Tým sme vyriešili konfiguráciu služby podľa metodológie The Twelve-Factor App.

Healthcheck

Spravovať distribuované systémy je náročné. Skladajú sa totiž z veľkého množstva súčastí, ktoré potrebujú pracovať (a spolupracovať), aby systém mohol ako celok fungovať.

Ak sa niektorá súčasť pokazí, systém ju musí:

  • detegovať, napr. vytvorením alertu,
  • obísť ju, napr. load balancer nebude posielať požiadavky na nefungujúcu službu, a
  • opraviť ju, resp. zotaviť sa z chybového stavu.

A to všetko sa musí udiať automagicky. Ako teda detegovať, že bežiaca služba nedokáže spracovávať požiadavky?

Čo je to Healthcheck?

Health check(teda “kontrola zdravia”) poskytuje jednoduchý mechanizmus, pomocou ktorého je možné overiť, či daná služba pracuje správne. obyčajne má služba vytvorené Health Check API pomocou protokolu HTTP (napr. /health), ktorá pomocou HTTP stavových kódov hovorí o stave služby. Je možné sa však stretnúť aj s prípadmi, kedy sa kontrola stavu služby vykonáva pomocou metódy HEAD nad niektorým existujúcim endpointom.

Okrem stavového kódu sa môže vo výsledku HTTP požiadavky nachádzať aj krátky JSON dokument, ako napr.:

{
    "status" : "UP"
}

What to Check in Health Check API

v rámci kontroly zdravia služby sa môže overovať napr.:

  • stav spojenia s ostatnými službami,
  • stav stroja, napr. jeho diskový priestor, využitie pamäte a pod.,
  • špecifická logika aplikácie,
  • stav databázy,
  • a iné.

Z-Pages

Ako endpoint pre Health Check API sa dá stretnut s názvom /healthz, ktorý používa napr. Grafana S týmto konceptom prišiel Google a endpoint-y, ktoré končia s písmenom Z sa nazývajú Z-Pages. Ide o to, aby nedošlo k zameneniu názvov množného cisla endpointov REST API, aby bolo jasné, že sa jedná o špeciálny endpoint, ktorý slúži primárne pre administráciu a monitoring, než pre bežných užívateľov.

Jednoduchý healthcheck

@app.head('/api/healthz')
@app.get('/api/healthz')
def check_health():
    return {
        'status': 'up'
    }

Health Check in Containers

Okrem toho však s kontrolou stavu súvisí aj kontrola stavu kontajnera, v ktorom je aplikácia spustená. kontajnery totiž bežia 24/7 a aj tu môže dôjsť k niektorým situáciám:

  • stav kontajneru
  • aktualizácia aplikácie v kontajneri

Takže ak aplikácia beží v kontajneri a ten je spustený, nemusí to hneď znamenať, že aplikácia vo vnútri kontajneru pracuje správne. Kontrolu teda treba vykonávať ako na úrovni aplikácie, tak aj na úrovni kontajnera.

na urovni kontajnerov mame podporu priamo pri definicii obrazu v súbore Dockerfile. k dispozícii je klucove slovo HEALTHCHECK a kontrola moze vyzerat napriklad takto:

HEALTHCHECK \
  --interval=30s \
  --timeout=3s \
  --retries=3 \
  --start-period=0 \
  CMD curl --fail http://localhost:8000/api/health || exit 1

Vyznam jednotlivých volieb kľúčového slova HEALTHCHECK je nasledujúci:

  • --interval=<interval> - Interval medzi dvoma kontrolami, predvolena hodnota je 30 sekúnd,
  • --timeout=<time length> - Prikaz na kontrolu sa bude vykonavat zadany cas. Ak tento cas uplynie, kontrola zdravia bude považovaná za neuspesnu. Predvolena hodnota je 30 sekúnd,
  • --retries=<number> - Ak dany pocet krat po sebe kontrola zdravia zlyhá, kontajner bude považovaný za nezdravy. Predvolena hodnota su 3 pokusy,
  • --start-period=<duration> - Po akom čase sa má kontrola spustiť. Tento čas je určený pre naštartovanie samotnej aplikácie.

Ako vyzerá taký stack ?

Väčšina aplikácií pozostáva nielen z jednej služby ale rovno z viacerých. Štandardne sa jedná o oddelenie častí ktoré vykonávajú operácie a častí ktoré ukladajú dáta. V praxi teda bežne aplikácia pozostáva z 3 a viac kontajnerov. Štruktúra môže byť nasledujúca:

Ingress

Rieši load balancing, SSL a publikovanie aplikácie von - napr. NGINX.

Aplikácia

Robí robotu :).

Databáza/Úložisko

Zabezpečuje uloženie dát.

Load Balancing našej služby

V našom prípade môžeme naraziť na problém, kedy počas získavania odpovede pre jedného používateľa je služba buď veľmi pomalá alebo aj nedostupná pre iných používateľov. Práve v tomto momente vieme využiť mágiu kontajnerov - plynulé škálovanie.

Avšak pre distribúciu sieťovej prevádzky jednotlivým kontjanerom budeme potrebovať ďalšiu službu - load balancer. Ten sa postará o rovnomernú distribúciu prevádzky.

V tomto momente bude mať už naša aplikácia 2 služby a teda je vhodné ich zapísať spoločne do manifestu, aby sme ich vedeli spúšťať ako celok a aby sme vedeli definovať čo s čím súvisí.

Takémuto súboru sa hovorí docker-compose.yaml alebo compose.yml:

services:
  app:
    build: ./app
    env_file:
      - ./app/.env
    expose: 
      - "8000"
  load_balancer:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./load-ballancer/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

Záver

Bude dopísané po prednáške na základe spätnej väzby.

Ďalšie zdroje