Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Ketrecbe zárt JVM - Java alkalmazás futtatása Docker containerben

Szűrő megjelenítése

Ketrecbe zárt JVM - Java alkalmazás futtatása Docker containerben

Röviden a Dockerről

A Docker operációs rendszer szintű virtualizációs megoldás, mellyel úgynevezett containereket hozhatunk létre alkalmazásunkból. Natív támogatás gyakorlatilag minden fontosabb platformon elérhető, így Windows, Linux és Mac OS rendszereken is belevághatunk containereink létrehozásába - bár éles környezetben leginkább Linux alatt futó Docker Engine-nel fogunk találkozni. Mindamellett, hogy önállóan is egy kiváló eszköz alkalmazásunk üzemeltetésére, további, sokkal komplexebb rendszerek alapja is lett az elmúlt évek során, így a Docker például a Kubernetes egyik leggyakrabban használt container runtime-ja is egyben.

De egyáltalán miért jó az nekünk, hogy container-be csomagoljuk az alkalmazásunkat, miért jó ez az egész virtualizálás? Nos, a mai szoftver fejlesztési trendek azt diktálják, hogy az alkalmazás fejlesztése és üzemeltetése lehetőleg egyazon emberek kezében legyenek, vagy legalábbis a fejlesztőcsapatok maguk dönthessék el, milyen környezetben és hogyan akarják futtatni az elkészült terméket. Ennek egyik komponense az előző cikkemben tárgyalt CI/CD pipeline önálló kialakítása, másik komponense pedig a pipeline végén az alkalmazás üzemeltetése. Természetesen ha az alkalmazást natív formájában (jar, py/pyc, ts, js, exe, vagy bármi más, a használt nyelvnek megfelelő executable formátum) akarjuk futtatni, a futtató szervert fel kell készíteni arra. Különböző runtime-ok telepítése válik szükségessé, egyes esetekben akár egyazon runtime több különböző verziójának jelenléte is indokolt lehet. Természetesen a szervereket karbantartó, üzemeltető csapatok senkinek nem fogják megengedni, hogy saját kedvére telepítgessen bármit a szóban forgó gépekre, így a megoldás keresése során eljutunk a containerek témaköréhez.

Egy (Docker) container az alkalmazásunk futtatható binárisát, a hozzá szükséges runtime-ot és egy általában erősen lecsupaszított, komplett operációs rendszert tartalmaz. A container elindításával felpörög a becsomagolt OS, azon és a hoston futó virtualizációs rétegen keresztül valós hardware erőforrások kerülnek kiosztásra a containernek (és természetesen ezek mennyisége is szabályozható), illetve elindul a belépési pontja a containernek, mely ilyen esetekben többnyire saját alkalmazásunk binárisa. Ezen a ponton az alkalmazás egy biztonságos, számára pontosan megfelelő környezetben fut, ráadásul csak olyan erőforrásokat ér el, melyeket valóban biztosítani is akarunk számára. A hosttal (és a hoston futó többi containerrel) úgynevezett volume-okon, valamint a nyitott portokon (és konfigurációtól függően egy privát hálózaton keresztül) keresztül képes adatokat megosztani.

Komponensek

A kérdés már csak az, hogyan kezdjünk hozzá, mire lesz szükségünk? A továbbiakban ezeket vesszük sorra.

Az alkalmazás

Természetesen a legfontosabb dolog, hogy legyen egy alkalmazásunk, amit containerizálni (magyarul szörnyen hangzik ez a szó) akarunk. Ez virtuálisan bármi lehet, persze nyilván inkább szerver alkalmazásokban fogunk gondolkodni. (Windows Docker alap image-ekkel elvileg Windowsra írt alkalmazások is futtathatóak Dockerben, személy szerint én még nem próbáltam, kommentben szívesen várnám a tapasztalatokat, ha bárki találkozott már Windows containerrel.) A Docker hivatalos, Maven Repository-hoz hasonló nyílt registryjében számos alap image-et találhatunk, különböző runtime-okhoz, különböző verziókkal, de elkészíthetjük saját alap image-ünket is, így valóban szinte bármilyen környezetet igénylő alkalmazást futtathatunk. Fontos megjegyezni, hogy a containerbe már csak a lefordított futtatható binárist, vagy production-ready interpreteres kódot érdemes elhelyezni - szóval nyers forráskód, teszt csomagok, oda nem illő teszt alkalmazás konfiguráció, stb nem való oda. Fontos megérteni, hogy a containerizáció után a container válik az alkalmazássá, azt ilyen mentalitással szükséges kezelni.

A Dockerfile

A folyamat legfontosabb (és legtöbb munkát igénylő) része a Dockerfile elkészítése, ami tulajdonképpen a containerünk specifikációja. A specifikáció minden esetben egy már létező parent image definiálásával kezdődik - ez a parent image lehet egy operációs rendszer, egy hasonló custom image, amit ki szeretnénk bővíteni pár extra lehetőséggel, stb. (Felmerülhet a kérdés, hogy ha minden imagenek kell egy parent image, akkor hogyan készült az első? A kíváncsiak számára a választ a Docker Scratch image jelenti, bár - ha csak nem saját operációs rendszer image-et akarunk csomagolni - ezt soha nem fogjuk használni.) A Docker Hub kínálatában szinte biztosan megtaláljuk az alkalmazásunk számára megfelelő runtime image-et, például Java-hoz is elérhető az összes jelenleg támogatott verzió csak JRE-t, illetve JDK-t is tartalmazó csomagja. Illetve a verziókat tekintve az adott főverzión belül maradva minden újracsomagolásnál megkaphatjuk a legfrissebb patch-et, ha az image tag-jeként csak a főverziót választjuk. Szóval a lényeg, hogy saját alap image-et ritkán kell majd készítenünk, a specifikáció inkább csak a saját alkalmazásunk konfigurálását fogja teljesíteni.

Mielőtt továbbmennénk, az alábbi példa a Leaflet Stack Admin Service Dockerfile specifikációját mutatja be - természetesen végigmegyünk a file felépítésén és megnézzük, melyik parancs mire szolgál.

FROM openjdk:11-jre-slim

ARG APP_USER=leaflet
ARG APP_HOME=/opt/lsas
ARG APP_EXECUTABLE=leaflet-sas-exec.jar
ENV ENV_APP_EXECUTABLE=$APP_EXECUTABLE

RUN addgroup --system --gid 1000 $APP_USER
RUN adduser --system --no-create-home --gid 1000 --uid 1000 $APP_USER
RUN mkdir -p $APP_HOME
ADD web/target/$APP_EXECUTABLE $APP_HOME
ADD config/leaflet-sas-exec.conf $APP_HOME

WORKDIR $APP_HOME
RUN chmod 744 $APP_HOME
RUN chmod 744 $APP_EXECUTABLE
RUN chown -R $APP_USER:$APP_USER $APP_HOME

USER $APP_USER

ENTRYPOINT ./$ENV_APP_EXECUTABLE ${APP_ARGS}

Mint az látható, a Dockerfile egyszerű parancsokat tartalmaz PARANCS <paraméter> formátumban. Az első sorban a már említett alap image kiválasztás látható (ebben az esetben a Java runtime image 11-es verziójának slim változatát használjuk majd), majd számos további parancs látható. Az ARG parancsokkal a scriptben használt konstansokat lehet definiálni (nem fontos, de a duplikációkat így ki lehet kerülni a script szövegében), míg az ENV paranccsal környezeti változó alap értéket definiálhatunk. A szemfülesebbek észrevehették, hogy az ENV_APP_EXECUTABLE csupán átemeli az APP_EXECUTABLE értékét - miért van erre szükség? Nos a válasz az, hogy az ARG-gal definiált konstansok csak az image fordítása során elérhetőek, míg az ENV változók futásidőben is. A script ENTRYPOINT parancsa már futásidőben kerül végrehajtásra, így ott nem volna elérhető a konstans értéke.

Persze ennyire ne szaladjunk előre, a köztes sorokban azért még számos dolog történik. Látható egy csomó RUN parancs. Ezekkel a containerben futó operációs rendszeren tudunk parancsokat végrehajtani, így pontosan azt kell tennünk velük, mintha egy terminál előtt ülnénk, és próbálnánk előkészíteni a szervert az alkalmazásunk futtatására. Ha ehhez egy runtime telepítése szükséges, akkor például RUN apt-get install .... Ha ehhez jogosultságokat, tulajdonost kell változtatnunk, akkor RUN chmod ... vagy RUN chown .... Fontos megemlíteni, hogy minden RUN parancs egy új úgynevezett réteget hoz létre az image-ben, ami az újrafelhasználhatóságot javítja, méghozzá oly módon, hogy ha az image egy adott rétege nem változott, akkor azt nem fordítja újra. Az ADD parancsokkal a projekt mappájából tudunk a containerbe másolni fájlokat, így kerül be például a lefordított végrehajtható jar file is a containerbe. Az ADD helyett használható a COPY is - előbbi például URL-t is kaphat paraméterül; a teljes funkcionalitását érdemes megnézni a hivatalos dokumentációban. A WORKDIR parancs mappát vált (szóval a cd megfelelője), a USER a végrehajtó felhasználót változtatja meg. Ez utóbbi nagyon fontos, hiszen enélkül az alkalmazás (illetve az egész container) root jogokkal fut.

A specifikációt az ENTRYPOINT zárja, mellyel a container belépési pontját határozhatjuk meg. Ebben az esetben-ben a jar file-t fogja közvetlenül futtatni (Spring Boot executable jar), illetve az APP_ARGS környezeti változóban kapott értékeket fogja átadni neki a futtatás során (itt például a profil nevét kapja majd meg). Az ENTRYPOINT helyett használható még a CMD is. A különbség nagyjából annyi, hogy az előbbi egy kötelezően futtatandó belépési pontot definiál, míg utóbbi könnyen felülbírálható alapértelmezett parancs a container indítása során (bár egy kapcsolóval az előbbi is felülbírálható).

A konfiguráció az image lefordításához

A fenti image kézzel már könnyedén fordítható lenne. Mivel a kérdéses projekt Mavennel van kezelve, mvn clean install parancs után a következő paranccsal tudjuk magát a Docker image-et elkészíteni (legyen az image neve lsas, verziója, 1.0):

docker build -t lsas:1.0 .

A nem túl bonyolult parancs két nagyon fontos dolgot is tartalmaz. Egyrészt a -t (tag) kapcsolóval megadhatjuk az image nevét (és verzióját, kihagyva azt a latest verzióra fog hallgatni az image), illetve a . ebben az esetben az aktuális könyvtárra hivatkozik, mint az úgynevezett build context. Ha nem pont a projekt gyökerében állunk (vagyis nem ott, ahol a Dockerfile található), a pont helyett a megfelelő elérési út megadására lesz szükség. Ez így azonban nem túl automatizált, szóval ugorjunk is vissza a Mavenhez, amihez természetesen rendelkezésünkre áll néhány egészen kényelmesen használható plugin a folyamat automatizálására. Mivel a Docker image-ek létrehozására a standard a Dockerfile használata, a Spotify Dockerfile pluginjára esett a választásom. Pusztán azért, mert a háttérben éppen csak annyit csinál, amit kell: elkészíti az image-et a Dockerfile alapján.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- ... -->

    <build>
        <finalName>leaflet-sas</finalName>
        <plugins>
            <!-- további pluginok -->
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>dockerfile-maven-plugin</artifactId>
                <version>${dockerfile-maven-plugin.version}</version>
                <executions>
                    <!-- két futtatást definiálunk -->
                    <execution>
                        <!-- az első lefordítja az image-et és tageli a projekt aktuális verziójával -->
                        <id>tag-with-project-version</id>
                        <goals>
                            <goal>build</goal>
                            <goal>push</goal>
                        </goals>
                        <configuration>
                            <tag>${project.version}</tag>
                        </configuration>
                    </execution>
                    <execution>
                        <!-- a második már csak tageli "latest" verzióval ugyanazt az image-et -->
                        <id>tag-with-latest</id>
                        <goals>
                            <goal>tag</goal>
                            <goal>push</goal>
                        </goals>
                        <configuration>
                            <tag>latest</tag>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <!-- a repository paraméter az image neve és opcionálisan egy repository szerver címe -->
                    <repository>${docker.repository}/apps/${docker.image-name}</repository>
                    <!-- ezzel utasítjuk a plugint, hogy a repository szerver credentialjeit a Maven settings.xml-jében találja -->
                    <useMavenSettingsForAuth>true</useMavenSettingsForAuth>
                    <!-- mivel ez a config a web modulban van, a Dockerfile és a build context is egyel fentebb lesz -->
                    <dockerfile>../Dockerfile</dockerfile>
                    <contextDirectory>../</contextDirectory>
                    <skip>${docker.skip}</skip>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

A fenti konfigurációval a mvn package parancsra az image elkészül, míg a mvn deploy paranccsal az image egy távoli repository szerverre is feltöltésre kerül. Ezutóbbihoz a szervert a settings.xml fájlban konfigurálnunk kell, ahol az ID lesz a szerver címe, illetve a username és password értelemszerűen kitöltendő.

A fenti konfiguráció fontos része az, ahogyan a "repository" érték meg van adva. Alapvetően oda elég lenne, ha az image nevét adjuk meg (lsas), de ebben az esetben két további, egymástól per jellel elválasztott komponense is lesz a névnek. A teljes image név így egy registry szerver címéből, egy csoport vagy szervezet (organization) nevéből (ez egyébként opcionális) és az image saját nevéből áll. Ezzel a Docker Engine-t arra készítjük fel, hogy egy custom registry szerverre töltjük majd fel az image-et. A registry szerver címével kapcsolatban annyit érdemes megemlíteni, hogy az URL pontot kell tartalmazzon (ez a kliensben egy kis "hiba"), különben nem tekinti szervercímnek az image név első per jele előtti részét. Így például localhoston futó registry szerver esetében a "localhost" cím helytelen lesz, helyette a 127.0.0.1-t kell használni. Az image nevéhez még hozzá fog rendelni a Docker egy tag-et (lényegében az image verzióját), amit ha külön nem adunk meg, akkor "latest" lesz, egyébként pedig a megadott tag. Ez az image használatakor kettősponttal elválasztva jelenik meg a név végén.

A container futtatása

Az image ezen a ponton már készen áll, benne a becsomagolt alkalmazásunkkal. Már csak az van hátra, hogy elindítsuk azt. Több módon is megtehetjük, a legérdekesebb talán a Docker Engine API használata, de az egy külön cikket is megérne - lehet egyszer el is készül majd. Alapesetben a Docker CLI és a Docker Compose lesz a két választási lehetőségünk.

Tegyük fel, hogy az alkalmazás a 8080-as porton kommunikál, a containeren kívül viszont a 80-as portot szeretnénk használni, logokat ír le a saját home könyvtára alatt található logs mappába, illetve, fogad egy külső konfigurációs fájlt. Mivel service-ként szeretnénk futtatni a containert, hasznos lenne, ha az Engine indulásakor a container is automatikusan elindul, így ezt is konfigurálni fogjuk. Továbbá az alkalmazás paraméterben meg kell kapja a profilt, meg egy külső konfigurációs fájlt, amit kívülről biztosítunk a container számára, egy read-only mount segítségével. Az alábbi Docker CLI parancs a fenti konfigurációt fogja elérni:

docker run \
  --detach \ # röviden: -d
  --name lsas_container \ # egyedi nevet rendel a containerhez
  --publish 80:8080 \ # külső_port:belső_port, röviden: -p
  --restart unless-stopped \ # így a container automatikusan újraindul, ha nem fut, kivéve, ha szándékosan lett leállítva
  --env APP_ARGS="--spring.profiles.active=production --spring.config.location=/opt/lsas/application.yml" \ # röviden: -e
  --volume "/var/logs/lsas:/opt/lsas/logs" \ # külső_útvonal:belső_útvonal:opciók, röviden: -v
  --volume "/config/lsas_application.yml:/opt/lsas/application.yml:ro" \ # ro = read-only mount
  custom.registry:5000/apps/lsas:1.2.0 # a container az itt megadott image-ből fog elkészülni

A másik lehetőség, ami egyébként elősegíti a Configuration as Code nézet fenntartását is, az a Docker Compose használata. Ehhez egy docker-compose.yml nevű fájlt kell létrehoznunk, definiálva benne a fenti paramétereket. Ezután a docker-compose up -d (a -d ez esetben is a detached módot jelenti) parancs használatával tudjuk indítani a containert. A fájl tartalma az alábbi lesz:

version: "3"
services:
  lsas_container:
    image: custom.registry:5000/apps/lsas:1.2.0
    ports:
      - 80:8080
    environment:
      APP_ARGS: |
        --spring.profiles.active=production
        --spring.config.location=/opt/lsas/application.yml
    volumes:
      - /var/logs/lsas:/opt/lsas/logs
      - /config/lsas_application.yml:/opt/lsas/application.yml:ro
    restart: unless-stopped

A container elindítása után az alkalmazásunk készen áll a használatra, és a példánál maradva (a port átirányítás miatt) a 80-as porton várja majd a requesteket.

Saját Docker Registry használata

Az image-ek tárolása idővel (igazából valószínűleg rögtön, ahogy belevágunk alkalmazásunk containerizálása) indokolttá válhat, amire alapvetően a hub.docker.com kínál lehetőséget. Itt azonban csak egy darab privát repository-t tárolhatunk, a többi publikus láthatóságon marad - persze ez előfizetéssel orvosolható. Azonban ha rendelkezünk saját szerverrel, ahova tudunk telepíteni egy repository servert, akkor mindjárt jobb lehetőség a hivatalos Docker Registry telepítése és használata. A projekt részletes dokumentációt biztosít, és annak ellenére, hogy ingyenes, teljeskörű funkcionalitást ad.

Docker dokumentáció

Kommentek
Hozzászólok

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