Profiling Java applicaties in docker container via ssh

In: Tutorial
Er zijn verschillende manieren om Java-profileringsmethoden toe te passen, elk met zijn eigen voor- en nadelen. In dit geval is gekozen voor Java-profilers die via een SSH-verbinding een agent naar de container kunnen kopiëren en starten. Hierdoor blijft alle logica in een herbruikbaar, lichtgewicht Docker-image, zonder de Java-ontwikkelomgeving zelf te belasten.
Het nadeel is dat je een extra Docker-image moet onderhouden, en dat deze ook op de productieomgeving draait. Een betere aanpak voor productieomgevingen is om eenmalig een profiler broker-agent te installeren en die te gebruiken om je applicatie te onderzoeken. Dit valt echter buiten de scope van dit onderwerp, omdat de focus hier ligt op niet-productieomgevingen.
Voor het gebruik van profiling via SSH zijn enkele stappen vereist, afhankelijk van je ontwikkelplatform. In dit voorbeeld gebruiken we GitLab als ontwikkelplatform en JProfiler als remote profiler. Daarom is voorkennis van SSH, GitLab, shell scripting, Java en Maven een pre.
Docker image met sshd
Er zijn verschillende Java Docker-images beschikbaar, zowel met als zonder SSHD. Het doel hier is om een zo compact mogelijk Docker-image te creëren. Tijdens het bouwen en testen bleek dat de JRE-versie van dit image niet voldeed, daarom is hier gekozen voor de JDK-versie. Dit image installeert een basisversie van SSHD, die via een opgegeven poort benaderd kan worden.
De laatste regel kopieert een SSHD-configuratiescript, maar dit script wordt niet automatisch uitgevoerd. Dit geeft je de flexibiliteit om later te beslissen of je het wel of niet wilt draaien, afhankelijk van de omgeving. Bijvoorbeeld, op een productieomgeving wil je dit script mogelijk niet uitvoeren, terwijl het in acceptatie-, test- of ontwikkelomgevingen wel nuttig kan zijn.
Dockerfile:
FROM eclipse-temurin:21-jdk-alpine
RUN apk add --update --no-cache openssh
EXPOSE 6060
COPY run-sshd.sh /
Het Docker-image verwijst naar een run-sshd.sh-shellscript, waarin de SSHD-configuratie plaatsvindt. Deze configuratie gaat ervan uit dat je de SSH-verbinding als root wilt opzetten. Het configuratiebestand sshd_config wordt aangepast zodat de SSH-daemon toegang biedt voor root via een versleutelde verbinding met een public key.
Het bestand authorized_keys bevat de public keys van gebruikers die SSH-toegang krijgen. In de meeste gevallen wil je dit bestand niet direct in je Docker-image opnemen, zeker niet als het image openbaar is. De authorized keys injecteer je via GitLab-secrets op het moment dat het Docker-image wordt gedeployed in een omgeving. Meer hierover volgt later.
run-sshd.sh:
!/bin/sh
ssh-keygen -A
#configure sshd for root
sed -i -e 's/#AllowAgentForwarding yes/AllowAgentForwarding yes/g' /etc/ssh/sshd_config
sed -i -e 's/AllowTcpForwarding no/AllowTcpForwarding yes/g' /etc/ssh/sshd_config
sed -i -e 's/GatewayPorts no/GatewayPorts yes/g' /etc/ssh/sshd_config
echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config
echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config
echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
echo 'Port 6060' >> /etc/ssh/sshd_config
mkdir ~/.ssh
cp /authorized_keys ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
passwd -u root
#start
/usr/sbin/sshd -D -e "$@" &
Gitlab
Je kunt dit Java Docker-image als project toevoegen in GitLab. Dit doe je door een nieuw GitLab-project aan te maken en de Dockerfile en run-sshd.sh toe te voegen aan de directory src/main/docker binnen dat project. In de rootdirectory kan het .gitlab-ci.yml-bestand worden aangepast. De verwijzingen naar ocs/docker-images/Java21 moeten uiteraard worden aangepast naar het nieuwe Docker-project.
Deze configuratie zorgt ervoor dat op basis van de inhoud van src/main/docker een Docker-image wordt aangemaakt en gepusht naar de GitLab Docker-repository. Let daarbij vooral op de $CI_REGISTRY…-waarden, die de koppeling met de GitLab Docker-registry specificeren. Deze waarden zijn later nodig om het Docker-image te gebruiken.
.gitlab-ci.yml:
image: docker:git
services:
- docker:dind
stages:
- build
build-and-push:
stage: build
tags:
- docker
services:
- docker:dind
script:
- >
DATE=$(date +%Y%m%d.%H%M)
- BRANCH=$(echo $CI_COMMIT_BRANCH | sed 's|^rel/||g' | sed 's|[.]|_|g' | awk -F/ '{ print $NF }')
- GIT_SHORT_COMMIT=$(echo $CI_COMMIT_SHA | cut -c1-8)
- REVISION=$DATE.$BRANCH.$GIT_SHORT_COMMIT
- >
echo "\nREVISION = $REVISION"
- >
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY/ocs/docker-images/java21:$REVISION src/main/docker
- docker push $CI_REGISTRY/ocs/docker-images/java21:$REVISION
Dit Docker-image kun je ook als basis gebruiken voor alle Java-applicaties in je GitLab-projecten. Wanneer je in GitLab een release maakt, wordt automatisch een ‘latest’-versie aangemaakt. Dit is handig als je wijzigingen aanbrengt in je Docker-image, omdat alle afhankelijke projecten deze updates dan automatisch oppakken. Wil je echter een specifieke versie gebruiken, dan kun je eenvoudig de revisie van de gewenste build selecteren.
Gebruik van het docker image
Voor het gebruik van het Docker-image gaan we ervan uit dat je een GitLab-project hebt voor je Java-applicatie. Er zijn meerdere manieren om een Docker-image te gebruiken. De eerste methode is via de Jib Maven-plugin. Deze plugin beheert alle Docker-aspecten en zorgt ervoor dat er een Docker-image van de Java-applicatie wordt samengesteld en gepusht naar de GitLab-repository.
De configuratie van de jib-meven-plugin versie is dan als volgt:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>${jib-maven-plugin.version}</version>
<executions>
<execution>
<id>install</id>
<phase>install</phase>
<goals>
<goal>buildTar</goal>
</goals>
<configuration>
<from>
<image>registry.Gitlab.com//ocs/docker-images/java21:latest</image>
<auth>
<username>${env.CI_DEPLOY_USER}</username>
<password>${env.CI_DEPLOY_PASSWORD}</password>
</auth>
</from>
<container>
<creationTime>${maven.build.timestamp}</creationTime>
<mainClass>org.estatio.webapp.EstatioApp</mainClass>
<ports>
<port>8080</port>
<port>5005</port>
<port>6060</port>
</ports>
<entrypoint>/entrypoint.sh</entrypoint>
</container>
<extraDirectories>
<permissions>
<permission>
<file>/entrypoint.sh</file>
<mode>755</mode>
</permission>
</permissions>
</extraDirectories>
<to>
<image>${env.IMAGE_NAME}</image>
<tags>
<tag>${env.REVISION}</tag>
</tags>
</to>
</configuration>
</execution>
</executions>
</plugin>
Ook hier moeten de verwijzingen naar ocs/docker-images/java21 uiteraard worden aangepast naar het nieuwe Docker-project. De autorisatie verloopt via de omgevingsvariabelen CI_DEPLOY…, die moeten overeenkomen met de $CI_REGISTRY…-waarden. Daarnaast kan de verwijzing naar entrypoint.sh in de directory src/main/jib worden toegevoegd of aangevuld.
Aanvullingen entrypoint.sh:
ln -sf /run/secrets/*info /info
while read -r line; do
environment=$(echo $line | cut -d= -f2)
done < "/info"
echo ""
echo "environment=$environment"
echo ""
if [ $environment = "dta" ];
then
echo ""
echo ""
echo "cp /run/secrets/authorized_keys /authorized_keys"
echo ""
cp /run/secrets/authorized_keys /authorized_keys
echo ""
echo ""
echo "starting sshd"
echo ""
chmod +x /run-sshd.sh
/run-sshd.sh
fi
Gitlab kan gebruikt worden om in de secrets een info bestand te genereren met welke omgeving je wilt deployen zoald ‘prod’, ‘acceptatie’, ‘test’, ‘dev’ of ‘dta’. Dat bestand kan vervolgens gebruikt worden om als variable in te lezen en de ‘authorized_keys’ te injecteren en het ‘run-sshd.sh’ script te runnen.
De tweede manier is gebruik te maken van de Eclipse JKube maven plugin. De gelijkenis met Jib maven plugin is groot. Alleen deze plugin is specifiek voor Kubernetes bedoeld.
De derde manier is een Dockerfile gebruiken die verwijst naar het Java21 docker image. Let wel dat dit alleen kan als je de Gitlab docker repostitory authorisatie gebruikt voordat je de docker build aanroept. Bijvoorbeeld op deze manier: ‘echo “$CI_DEPLOY_PASSWORD” | docker login $CI_REGISTRY -u $CI_DEPLOY_USER –password-stdin’
Connectie
Voor zowel JProfiler als YourKit zijn er wizards beschikbaar om een verbinding met een remote omgeving op te zetten. Deze processen zijn doorgaans rechttoe rechtaan. Wat niet altijd duidelijk wordt vermeld, is dat je bij het verbinden met een Docker-container vaak niet direct toegang hebt tot de container en je meestal via de host moet tunnelen. Hoewel deze profiler-applicaties ondersteuning bieden voor SSH-tunneling, is deze vaak vrij eenvoudig.
Als je meer geavanceerde functionaliteit nodig hebt, zoals het vinden van een specifieke container, kan het nodig zijn om een eigen shellscript te maken. Hiervoor heb je toegang tot de host nodig, wat niet per se root-toegang vereist, maar wel een aparte SSH-gebruiker met een public key op de hostserver. Dit is ook de reden waarom in de sshd_config-instellingen forwarding is ingeschakeld, zodat de SSHD via forwarding ook goed functioneert.
Dan is een voorbeeld van de ssh tunnel:
ssh -N -T -L "${LOCAL_PORT}":localhost:"${END_PORT}" ${SSH_USER}@"${HOST}"
In Kubernetes is dit simpel te realiseren via port forwarding.
Nieuwsgierig of maatwerk voor jou het verschil kan maken?
Neem contact met ons op voor maatwerkoplossingen in Java, geleverd door een ervaren team dat stabiliteit, continuïteit en optimalisatie garandeert voor jouw bedrijfsprojecten.