Kafka og eventdrevne arkitekturer er kraftige verktøy. Samtidig kan det fremstå som vanskelig å få grep om som utvikler. Ofte innebærer utviklingspraksis at man kjører direkte mot et delt test- eller utviklingscluster, som gjør at man som ny utvikler vegrer seg for å utforske i frykt for å ødelegge for andre. Min personlige favoritt-kafka-brannfakkel er at delte utviklingsmiljøer for Kafka *er en uting* - uansett erfaringsnivå. Delte utviklingsmiljøer er kostbare å vedlikeholde, og sjelden strengt nødvendige. Bli med på en "guided tour" gjennom et minimalt lokalt cluster!
Docker Compose
Docker Compose er et verktøy som lar deg beskrive et sett gjensidig avhengige containere i form av YAML. Verktøyet lar deg også styre dem som en enhet - perfekt til lokalt utviklingsbruk for økosystemer som Kafka. Docker Compose er fritt tilgjengelig, for eksempel via brew install docker-compose.
I denne guiden går vi gjennom oppsett av et enkelt-broker Kafka cluster med tilhørende skjemaregister, men man kunne enkelt sett for seg å legge til flere brokere og tilstøtende tjenester som Kafka Connect.
Kortversjonen: docker-compose.yaml
For å komme raskt i gang kan du bruke dette docker compose-oppsettet:
networks:
kafkanetwork:
#external: true
name: kafkanetwork
services:
kafka1:
image: confluentinc/cp-kafka:7.8.6
hostname: kafka1
container_name: kafka1
environment:
KAFKA_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://kafka1:9094,CONTROLLER://kafka1:9093
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://localhost:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_PROCESS_ROLES: 'controller,broker'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:9093'
KAFKA_LOG_DIRS: '/var/lib/kafka/data'
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
CLUSTER_ID: N40tqN41Qp-NcaeapaLCKQ
ports:
- "9094:9094"
networks:
- kafkanetwork
volumes:
- kafka1-data:/var/lib/kafka/data
schemaregistry1:
image: confluentinc/cp-schema-registry:7.8.6
hostname: schemaregistry1
container_name: schemaregistry1
restart: always
depends_on:
- kafka1
networks:
- kafkanetwork
ports:
- "8085:8085"
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka1:9092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schemaregistry1
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8085
volumes:
kafka1-data: Start opp den ved å kjøre docker compose up -d. For å sjekke status på containerene kan du bruke docker compose ps. Du kan også se direkte på loggene, for eksempel docker compose logs kafka1.
Bruk av containerne fra host
Siden vi eksponerer containerne på henholdsvis 9094 for Kafka og 8084 for skjemaregister vil du kunne nå disse direkte fra utviklingsmiljøet ditt på localhost:9094 og http://localhost:8084. Eksempelet over bruker ingen sikkerhetsmekanismer, så for Kafka spesifiserer man kun disse egenskapene:
bootstrap.servers=localhost:9094
security.protocol=PLAINTEXTDetaljert gjennomgang
For å kunne bygge videre på oppsettet over og gjøre det til ditt eget kan det være greit med en liten innføring i hva konfigurasjonen faktisk gjør. La oss gå litt i dybden!
Network
network definerer hvilket internt nettverk de ulike tjenestene skal tilhøre. For oss holder det med ett - la oss kalle det kafkanetwork.
network:
name: kafka-network
#external: trueExternal-flagget spesifiserer at nettverket er opprettet et annet sted, for eksempel med docker network create. Det er nyttig å slå på dersom du har organisert utviklingsmiljøet ditt i flere compose-filer, men ønsker å ha de resulterende containerne på samme nettverk.
Volumes
volumes definerer diskvolumer som containerne kan mounte. For vår bruk kan vi holde dette veldig enkelt:
volumes:
kafka1-data:Services
Med nettverk og lagring definert kommer vi til kjernen i det hele: Selve containerene.
services.kafka1
I dette testoppsettet bruker vi et Kafka cluster bestående av en enkelt broker. Dette er ofte nok til utviklingsarbeid, men kan utvides ved behov. La oss se på det hele eksempelet først, før vi går gjennom enkelte linjer mer fokusert:
services:
kafka1:
image: confluentinc/cp-kafka:7.8.6
hostname: kafka1
container_name: kafka1
environment:
KAFKA_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://kafka1:9094,CONTROLLER://kafka1:9093
KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka1:9092,EXTERNAL://localhost:9094
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT
KAFKA_PROCESS_ROLES: 'controller,broker'
KAFKA_NODE_ID: 1
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka1:9093'
KAFKA_LOG_DIRS: '/var/lib/kafka/data'
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false"
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
CLUSTER_ID: N40tqN41Qp-NcaeapaLCKQ
ports:
- "9094:9094"
networks:
- kafkanetwork
volumes:
- kafka1-data:/var/lib/kafka/dataVi kaller tjenesten kafka1, slik at vi gjør jobben enklere for oss selv om vi trenger utvide i fremtiden. Det finnes flere docker images man kan bruke; for min del foretrekker jeg som regel Confluent sine: image: confluentinc/cp-kafka:7.8.6
Listeners
KAFKA_LISTENERS definerer hvor Kafka lytter, og hvilket navn vi bruker på de forskjellige lyttterne i resten av konfigurasjonen. En listener er kort fortalt et endepunkt. Legg merke til at vi har flere forskjellige her, og at navnene er valgfrie så lenge de er konsistente med øvrig konfig.
De to viktigste er disse:
INTERNALbruker vi her for kommunikasjon innad i Docker-nettverketEXTERNALbruker vi for kommunikasjon utenfor Docker; for eksempel med utviklingsmiljøet ditt
Vi har også CONTROLLER, som brukes for koordinering av metadata mellom brokere.
KAFKA_ADVERTISED_LISTENERS definerer hvor Kafka skal fortelle klientene at den lytter. Når du først kobler til Kafka på bootstrap-urlen får du tilbake en respons med hvilke brokere som finnes hvor for listeneren du kobler deg opp mot. Videre trafikk går mot disse - som igjen betyr at de må ta hensyn til hva som fører til at klientapplikasjonen når frem til brokeren. I vårt tilfelle har vi to stykker, siden vi behøver å snakke med Kafka både fra innsiden og utsiden av Docker-nettverket:
INTERNAL://kafka1:9092bruker det interne hostnavnet på docker-nettverket, satt iservice.kafka1.hostnameEXTERNAL://localhost:9094dirigerer klienter til å spørre motlocalhost:9094, siden vi eksponerer 9094 på host-nettverket iservices.kafka1.ports
Øvrig konfigurasjon
De mest aktuelle variablene å skru på er antagelig disse:
KAFKA_LOG_DIRSdefinerer hvor Kafka skal lagre data. Merk at Kafka er det man kaller en commit log - log dirs refererer her til de faktiske dataene, ikke applikasjonslogger. Legg merke til at vi pekervolumestil samme sti lenger nede.KAFKA_AUTO_CREATE_TOPICS_ENABLEkan slås på for å gjøre det mulig å automatisk opprette topics dersom de mangler. Det er generelt ikke anbefalt å bruke dette i prod, så jeg pleier unngå det også i lokal dev for å unngå overraskelser senere.KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1setter standard replikeringsfaktor. Siden vi har en enkelt node er vi nødt til å sette 1 her, siden vi ikke har noen annen broker å replikere til. Dersom man senere legger til en ekstra broker kan denne økes for realismens del.
Resten av konfigurasjon er av (enda) mer teknisk art:
KAFKA_NODE_IDer den unike identifikatoren for noden i clusteret-
KAFKA_LISTENER_SECURITY_PROTOCOL_MAPdefinerer sikkerhetsprotokoller. For å holde dette eksempelet enkelt bruker viPLAINTEXTpå alt. KAFKA_INTER_BROKER_LISTENER_NAMEogKAFKA_CONTROLLER_LISTENER_NAMEdefinerer hvordan clusteret skal snakke mellom brokere (for replikering o.l.), og hvordan Controller-noden skal kommuniseres med.- I vårt tilfelle har vi kun en enkelt broker, men disse er verd å se nærmere på om man skulle ønske å utvide med flere brokers
CLUSTER_IDdefinerer hvilket cluster denne brokeren tilhører. Denne verdien kan genereres medbin/kafka-storage random-uuid, et verktøy som finnes i Kafka-distribusjonen samt i imaget.
services.schemaregistry
Skjemaregisteret er heldigvis en del enklere. Skjemaregister er teknisk sett ikke en påkrevd del av Kafka, men en veldig vanlig (og høyst anbefalt) del av økosystemet. Komponenten sparer en for feilbarlig manuell eller hjemmebrygget skjemahåndtering, med god støtte i standardbiblioteker. La oss se på det igjen:
schemaregistry:
image: confluentinc/cp-schema-registry:7.8.6
hostname: schemaregistry
container_name: schemaregistry
restart: always
depends_on:
- kafka1
networks:
- kafkanetwork
ports:
- "8085:8085"
environment:
SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: kafka1:9092
SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
SCHEMA_REGISTRY_HOST_NAME: schemaregistry
SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8085Vi setter depends_on: [kafka1] for å sørge for at skjemaregisteret starter opp etter at Kafka selv er klart. Grunnen til dette er at skjemaregisteret benytter Kafka som lagringsmedium; legg merke til mangelen på volumes. Skjemaregisteret sitt grensesnitt mot klienter er REST-basert, så vi trenger bare fortelle hvordan det skal koble til Kafka og hvor det skal lytte på HTTP. Merk at vi eksponerer 8085 for host-nettverket, som betyr at du fra utviklingsmiljøet kan nå skjemaregisteret på http://localhost:8085.
Oppsummering
Et lokalt utviklingsmiljø kan spinnes opp og tas ned kjapt ved hjelp av docker compose. Kafka har en del konfigurasjon som kan oppleves kompleks ved første øyekast, men man kan se bort fra mye av det ved oppsett av et enkelt utviklingsmiljø som dette.
Avslutningsvis er det på sin plass å nevne at dette ikke løser alt man bruker delte testmiljøer til, som delte data. Dette kan dog løses på andre måter - for eksempel ved å strømme avgrensede mengder testdata fra et staging-miljø til det lokale miljøet.