Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Docker integráció saját alkalmazással

Szűrő megjelenítése

Docker integráció saját alkalmazással

A Docker CLI egy meglehetősen kiterjedt funkcionalitású eszköz, mellyel a teljes Docker környezetünket vezérelni tudjuk. A helyzet egy kicsit bonyolultabbá válik viszont, amennyiben programozott módon, egy másik alkalmazásból szeretnénk vezérelni azt. Erre biztosít lehetőséget a Docker Engine API, mely egy standard REST API implementáció, és szerencsére teljeskörűen dokumentált. Az API-t nem csak a környezet enterprise kiadása támogatja, hanem része a Community Edition-nek is, így amennyiben saját projektünk Dockeresítése a cél, és ahhoz egy saját management tool fejlesztését tűznénk ki célul, bátran belevághatunk, az ingyenes kiadás nem fog minket ebben akadályozni.

Technikai oldalról az API elérése két módon lehetséges. Alapesetben UNIX socketen keresztül expose-olja a rendszer az API-t, mely mindig a /var/run/docker.dock útvonalon érhető el. Ennek egyetlen hátulütője, hogy nem használhatunk bármilyen HTTP kliens implementációt, csak olyat, mely támogatja a UNIX socketekhez való kapcsolódást. Előnye, hogy nem igényel további konfigurációt és mivel a socket csak a hoston belül érhető el, semmilyen lehetőség nincs annak külső elérésére - így tehát biztonságos, a műveletek elérhetősége kizárólag az API-ra épített alkalmazás elérhetőségén múlik. A másik lehetőség az Engine Daemon TCP exposure-je, melyet a Docker konfigurációs beállításaiban tehetünk meg. Az így expose-olt API localhoston, a 2375-ös porton válik elérhetővé, és lévén standard HTTP socket nyílik, bármilyen HTTP kliens implementációval elérhető lesz az API. Fontos megjegyezni, hogy a Docker nem igényel authentikálást az API-on, így amennyiben a portot például proxy használatával publikusan elérhetővé tesszük, az egy súlyos biztonsági rés megnyitásával egyenértékű - ne feledjük, az API minden műveletet támogat, amit a CLI is.

A továbbiakban néhány commandot illetve azok használatát szeretném bemutatni, párhuzamot vonva a CLI-ben használható műveletekkel. Példakódok szempontjából most inkább a REST kérések path-át, request bodyját fogom bemutatni, de egy pár szóban a lehetséges kliens implementációkat is érintjük majd. A bemutatott commandok egy teljes Docker Container indítási flow-t alkotnak majd, kezdve az image letöltésével, majd folytatva a container létrehozásával, elindításával, leállításával és eltávolításával. Közben azt is megnézzük, hogyan lehet információt lekérni a futó containerről.

Docker Image letöltés

Az image letöltése a CLI-ben a docker pull command kiadásával lehetséges. Argumentuma az image neve, opcionálisan a registry host nevével prefixálva, illetve suffixként megadható az image tagje (ami tekinthető az image verziószámának). Így a teljes command az alábbiak szerint néz ki:

docker pull registry.localhost:5000/myapp:1.0

A fenti command a registry.localhost nevű hoston, az 5000-es porton hallgatózó Docker Registryből letölti a myapp image 1.0-as tagjét (verzióját). Ugyanez a Docker Engine API használatával a következőképpen alakul majd:

POST /v1.40/images/create?fromImage=registry.localhost:5000/myapp&tag=1.0

Szedjük szét kicsit a fenti requestet. Az első szembetűnő dolog, hogy a path az API verziójával kezdődik. Ez minden esetben így van, a jelenlegi legfrissebb API verzió az 1.40-es, így a path a v1.40 paraméterrel kezdődik. Majd a resource szerint egy image-et hozunk létre. Ami fura, mert az image-ünk már megvan, így az elnevezés szerintem kicsit megtévesztő. Mindenesetre több módon is létrehozhatjuk az image-et, ebben az esetben egy meglévő image letöltése lesz a cél, ezt jelzi a fromImage query paraméter. Az image neve, ha az nem a standard repositoryban található, kell, hogy tartalmazza a registry nevét is (pont, mint a pull CLI commandnál). A letöltés ott válik kicsit érdekesebbé, ha private repositoryból töltenénk le az image-et. Ehhez a CLI-vel egy docker login command kiadására van szükség, mellyel megtörténik a registrybe való bejelentkezési folyamat, a továbbiakban pedig a CLI elhelyezi az access token-t a registry felé irányuló pull kérésben.

A bejelentkezés, REST API-oknál szokásos módon headerrel történik, bár ebben az esetben nem a standard Authorization headert kell használnunk, hanem az X-Registry-Auth-t. Két lehetőség van, implementációtól függ, hogyan fogjuk használni. Az egyik lehetőség, hogy minden esetben küldjük az összes szükséges paramétert (felhasználónév, jelszó, szerver cím, opcionálisan email cím), ekkor nem szükséges külön authentikációs kérést küldenünk az /auth endpointra. A másik lehetőség, ha az előbb említett /auth endpointon authentikálunk előre, akkor csak a kapott tokent kell küldenünk a headerben. Mindkét esetre igaz, hogy egy teljes JSON objektumot kell létrehoznunk, és azt base64 encode-olva elhelyezni a headerben. További részletek az authentikációról a dokumentáció Authentication részében olvasható.

Docker Container létrehozás és indítás

A container elindítása CLI-vel egyetlen command, mégpedig a docker run utasítást kell kiadnunk, jópár paraméter társaságában. Valójában a run command két Engine API utasítást von össze, ami az alábbiak szerint fog kinézni.

POST /v1.40/containers/create?name=myapp_container

Tehát az első command a container létrehozása. A path csak egy paramétert tartalmaz, ami a container majdani neve lesz. Minden más paraméter a request bodyba kell kerüljön, amit kézzel összerakni, nos, nem a legmókásabb elfoglaltság. Vegyük alapul az előző cikkemben látható docker run commandot:

docker run \
  --detach \
  --name lsas_container \
  --publish 80:8080 \
  --restart unless-stopped \
  --env APP_ARGS="--spring.profiles.active=production --spring.config.location=/opt/lsas/application.yml" \
  --volume "/var/logs/lsas:/opt/lsas/logs" \
  --volume "/config/lsas_application.yml:/opt/lsas/application.yml:ro" \
  custom.registry:5000/apps/lsas:1.2.0

A fenti commandnak megfelelő request body pedig így néz majd ki:

{
  "Env": [
    "APP_ARGS=--spring.profiles.active=production --spring.config.location=/opt/lsas/application.yml"
  ],
  "HostConfig": {
    "Binds": [
      "/var/logs/lsas:/opt/lsas/logs",
      "/config/lsas_application.yml:/opt/lsas/application.yml:ro"
    ],
    "PortBindings": {
      "8080": [
        {
          "HostPort": "80"
        }
      ]
    },
    "RestartPolicy": {
      "Name": "unless-stopped"
    }
  },
  "Volumes": {
    "/opt/lsas/logs": {},
    "/opt/lsas/application.yml": {}
  },
  "ExposedPorts": {
    "8080": {}
  },
  "Image": "custom.registry:5000/apps/lsas:1.2.0"
}

Szóval igen, erre jobb egy automatizált megoldást elkészíteni. :) Ezt a kérést elküldve viszont még nincs futó containerünk, mivel a kérés csupán létrehozza, de nem indítja el a containert. A fenti command visszatéréseként megkapjuk a container ID-ját, ezt kell majd elküldenünk az alábbi, második kérésben:

POST /v1.40/containers/<containerID>/start

A container ezzel elindul a háttérben (szóval a detached mód mondhatni alapértelmezett). Amennyiben látni szeretnénk, mit csinál a container valós időben, egy másik command segítségével csatlakozni lehet a container standard outputjára, az endpoint pedig streaming adatként küldi vissza az üzeneteket.

Valós idejű container információk

A futó containerről több módon is információhoz juthatunk, néhány fontosabbról lesz szó alább:

GET /v1.40/containers/<containerID>/logs

A container által a standard outputokra küldött üzeneteket adja vissza streaming adatként, ha a follow query paramétert true-ra állítjuk. Ezutóbbi esetben fontos tehát, hogy az implementált kliens ne szakítsa meg rögtön a kapcsolatot, illetve legyen implementálva megszakítási lehetőség.

GET /v1.40/containers/<containerID>/json

Alacsony szintű container információkat ad vissza. Gyakorlatilag az indítási paraméterek mellett számos további paramétert megjelenít ez az endpoint, a dokumentáció ezt részletesen leírja.

GET /v1.40/containers/<containerID>/stats

Valós időben jelenít meg elsősorban erőforrás foglalási statisztikákat a futó containerről. Fontos megemlíteni, hogy a processzor terhelést nem közvetlen értékkent adja vissza, a dokumentáció részletezi azt az algoritmust, ami alapján egy standard százalékos CPU loadot meg tudunk állapítani. Hasonlóképp a foglalt memória mennyiségét sem adja vissza közvetlenül, arra is van egy egyszerű képlet, amit követnünk kell.

Container életciklus kezelése

A container elindítását már láttuk, leállítása, újraindítása, szüneltetése, stb. hasonlóképp történik. Minden esetben POST kérést kell küldenünk, a végén a megfelelő paranccsal, mely a restart, stop, pause, unpause, kill, stb lehet.

POST /v1.40/containers/<containerID>/restart
POST /v1.40/containers/<containerID>/stop
POST /v1.40/containers/<containerID>/pause
...

A container eltávolítása az alábbi módon történik (force query paraméterrel akár a futó container is egy utasításban eltávolítható):

DELETE /v1.40/containers/<containerID>

Kliens implementáció

Kliensek szempontjából, mint korábban említettem, olyan libraryre van szükségünk, ami képes kezelni UNIX socketeket is (már ha a socketen keresztüli kommunikációnál maradtunk). TCP-exposed módban semmi különleges nincs a kliens implementációjában, bármilyen kliens library megfelel. UNIX socket esetben már másabb a helyzet, nézzünk pár példát.

Egy triviális megoldás a curl Linux command használata, mely alapértelmezetten támogatja a UNIX socketekhez való kapcsolódást. Egy Docker Engine API command curl használatával nagyjából így néz ki (a példa egy container indítás parancs):

curl --unix-socket /var/run/docker.sock -X POST http:/v1.40/containers/<containerID>/start

Persze nem a legjobb megoldás, ha az alkalmazásunk OS commandokkal a curl utility-t hívogatja, így valami rugalmasabb megoldásra lesz szükségünk. A Domino nevű alkalmazásomban (melyről hamarosan lesz egy bemutatkozó cikk) a request nevű (azóta már sajnos deprecated) NodeJS HTTP kliens library-t használtam. A paraméterek használata semmiben sem különbözik, magát a host URI-ját leszámítva, mely az alábbiahoz hasonlóan kell kinézzen:

http://unix:/var/run/docker.sock:/v1.40/containers/<containerID>/start

Tehát az URI a unix kulcsszóval kezdődik (érdekes módon a protocol itt nem is feltétlenül számít, http teljesen megfelel), majd kettősponttal elválasztva a socket path következik. Végül újabb kettőspont után a resource path jön. Az erőforrás maga továbbra is standard REST API-ként viselkedik, így a HTTP metódusokat továbbra is az API-nak megfelelően kell használnunk.

Egy másik esetben Spring Boot alkalmazásban szerettem volna integrálni néhány funkciót (a fentebb bemutatott statisztikai / diagnosztikai funkciókat), ott pedig Spring WebFlux WebClient klienst használtam már más esetekben, így maradni szerettem volna ennél a kliensnél. Sajnos mint kiderült a WebClient nem támogatja a UNIX socketek használatát, így végül ott a TCP exposure lett volna a megoldás. Azonban mivel az alkalmazás maga Docker containerben fut és a socket mountolva volt a containerre, úgy döntöttem, a socat nevű Linux utility segítségével nyitok egy UNIX Socket -> TCP bridge-et, ami után már standard HTTP klienssel is csatlakozni tudtam - lényegében TCP exposure, de így csak a containeren belül történik. Java-ban egyébként ennyire kényelmes megoldást nem is találtam Unix Socket felé irányuló kapcsolat létrehozására - amennyiben tudtok ilyenről, kommentben megírhatjátok!

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.