Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
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.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.