Obrazy Dockera są łatwe w obsłudze. Nie musimy instalować konkretnej wersji środowiska, bibliotek i innych zależności. Wszystko powinno być zamknięte w abstrakcji zwanej kontenerem. Możemy je uruchamiać i skalować w Docker Swarm lub Kubernetes. W tym wpisie zajmiemy się dockeryzacją aplikacji Kafka Streams na przykładzie strumienia dla lokalizacji autobusów ZTM przedstawionym w poprzednim wpisie.
Zmiany w pom.xml
W pierwotnej wersji projektu utworzenie paczki komendą mvn package generowało thin jar-a, czyli takiego, którego użycie wymagało dostarczenia zależności. Wygodniej w naszym przypadku utworzyć fat jar, aby kompletna aplikacja znajdowała się w jednym pliku.
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
Zmiany w kodzie aplikacji Kafka Streams
W aplikacji są zahardkodowane nazwy topic-ów, id aplikacji i adres brokera kafki. Aby uniknąć kompilowania nowej wersji przy każdej zmianie, utworzymy mechanizm oparty o zmienne środowiskowe. Jest to dość często spotykany sposób konfigurowana kontenerów w Dockerze.
Utworzyłem klasę EnvTools, która zwraca zmienną lub podaną wartość domyślną. Trochę brakuje mi operatora ?? dostępnego w C# 😉 . Aby uniknąć literówek, utworzyłem statyczne stringi z kluczami.
public class EnvTools {
public static final String INPUT_TOPIC = "INPUT_TOPIC";
public static final String OUTPUT_TOPIC = "OUTPUT_TOPIC";
public static final String APPLICATION_ID_CONFIG = "APPLICATION_ID_CONFIG";
public static final String BOOTSTRAP_SERVERS_CONFIG = "BOOTSTRAP_SERVERS_CONFIG";
public static String getEnvValue(String environmentKey, String defaultValue)
{
String envValue = System.getenv(environmentKey);
if(envValue != null && !envValue.isEmpty())
{
return envValue;
}
return defaultValue;
}
}
Teraz gdy będę chciał zmienić jakiś parametr, wystarczy zmienna środowiskowa.
...
public ZtmStream() {
inputTopic = EnvTools.getEnvValue(EnvTools.INPUT_TOPIC, "ztm-input");
outputTopic = EnvTools.getEnvValue(EnvTools.OUTPUT_TOPIC, "ztm-output");
}
...
private static Properties createProperties() {
Properties props = new Properties();
String appIdConfig = EnvTools.getEnvValue(EnvTools.APPLICATION_ID_CONFIG, "wiaderko-ztm-stream");
String bootstrapServersConfig = EnvTools.getEnvValue(EnvTools.BOOTSTRAP_SERVERS_CONFIG, "localhost:29092");
props.put(StreamsConfig.APPLICATION_ID_CONFIG, appIdConfig);
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServersConfig);
...
Dockerfile
Dockerfile to plik zawierający kroki/instrukcje wymagane do utworzenia obrazu dockera. Przyjrzyjmy mu się.
FROM maven:3-openjdk-8-slim AS BUILD
COPY pom.xml /tmp/
COPY src /tmp/src/
WORKDIR /tmp/
RUN mvn package
FROM openjdk:8-alpine AS RUNTIME
RUN apk update && apk add --no-cache libc6-compat
RUN ln -s /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-x86-64.so.2
WORKDIR /app/
COPY --from=BUILD /tmp/target/wiadrodanych-kafka-streams-*-jar-with-dependencies.jar .
ENTRYPOINT ["java", "-cp", "*", "wiadrodanych.streams.ZtmStream"]
Pierwsza część odpowiada za zbudowanie aplikacji. Projekt wykrzystuje maven i jest napisany pod Javę 8, stąd podstawą jest maven:3-openjdk-8-slim. Kopiowane są koniecznie pliki, a następnie uruchamiana jest komenda mvn package, która tworzy plik .jar
Druga część jest to odchudzony kontener z openjdk w wersji 8. W momencie uruchamiania aplikacji nie potrzebujemy mavena, dlatego możemy zaoszczędzić na wielkości kontenera używając innej bazy pod kontener.
Wersja alpine nie posiada ld-linux-x86-64.so.2, przez co kafka streams się nie uruchomi, dlatego wykonywana jest instalacja paczki i pewien trick z linkiem. Następnie kopiowany jest utworzony wcześniej jar z poprzedniej warstwy. Kontener po odpaleniu wykona komendę java -cp * wiadrodanych.streams.ZtmStream, czyli załaduje wszystko, co znajdzie w aktualnym folderze i uruchomi klasę ZtmStream.
Zbudowanie obrazu Docker
Znajdując się w folderze gdzie jest plik Dockerfile
docker build -t ztm_stream .
Gdy zobaczymy Successfully tagged ztm_stream:latest to jesteśmy w domu. Obraz ma 133 MB, z czego czysty openjdk 105 MB. Wytrwawny wielorybiarz pewnie by coś jeszcze z tego przyciął :-).
Docker-Compose
Wrzućmy aplikację i kafkę wszystko docker-compose.yml i włączmy docker-compose up. Weź pod uwagę zmienne środowiskowe dla ztm_kafka_streams.
version: '3.3'
services:
zookeeper:
image: 'bitnami/zookeeper:3'
ports:
- '2181:2181'
volumes:
- 'zookeeper_data:/bitnami'
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: 'bitnami/kafka:2'
ports:
- '9092:9092'
- '29092:29092'
volumes:
- 'kafka_data:/bitnami'
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,PLAINTEXT_HOST://:29092
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
depends_on:
- zookeeper
ztm_kafka_streams:
image: "ztm_stream"
environment:
- APPLICATION_ID_CONFIG=awesome_overrided_ztm_stream_app_id
- BOOTSTRAP_SERVERS_CONFIG=kafka:9092
depends_on:
- kafka
volumes:
zookeeper_data:
driver: local
kafka_data:
driver: local
Działanie
Jak widać poniżej, działa :-). Skrypt python, którym wrzucam rekordy, jest tym samym, którego użyłem w poprzednim wpisie.
Repozytorium
https://github.com/zorteran/wiadro-danych-kafka-streams/tree/kafka_streams_202
Branch: kafka_streams_202
Jeden komentarz do “Kafka Streams 202 – Dockeryzacja aplikacji, czyli Kafka w kontenerze”