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
Poznámka
Slidy z prednášky sú dostupné tu
Motivácia
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:
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 :).
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:
Č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.
Upozornenie
Obrazy sú nemenné!
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-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.
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/mydatabasePremenné 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=mydatabaseUpozornenie
Súbor .env NIKDY!!! neposielajte do repozitára s vašimi
zdrojovými kódmi, pretože obsahuje citlivé informácie častokrát pre
prístup k vzdialeným službám alebo databázam! Ak sa takýto súbor do
repozitára dostane, je to považované za únik citlivých informácií. Preto
si do súboru .gitignore vždy pridajte záznam, ktorý zamedzí
odosielaniu týchto súborov do repozitára.
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=celsiusNá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 1Vyznam jednotlivých volieb kľúčového slova HEALTHCHECK
je nasledujúci:
--interval=<interval>- Interval medzi dvoma kontrolami, predvolena hodnota je30sekú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 je30sekúnd,--retries=<number>- Ak dany pocet krat po sebe kontrola zdravia zlyhá, kontajner bude považovaný za nezdravy. Predvolena hodnota su3pokusy,--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:
- appZáver
Bude dopísané po prednáške na základe spätnej väzby.
Ďalšie zdroje
- The Twelve-Factor App methodology
- Containers (Docker) vs Virtual Machines (VM): Pros, Cons, and Use Cases
- microservices.io Pattern: Health Check API