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
Jedna myśl w temacie “Kafka Streams 202 – Dockeryzacja aplikacji, czyli Kafka w kontenerze”