Hopp til hovedinnhold

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=PLAINTEXT

Detaljert 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: true

External-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/data

Vi 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:

  • INTERNAL bruker vi her for kommunikasjon innad i Docker-nettverket
  • EXTERNAL bruker 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:9092 bruker det interne hostnavnet på docker-nettverket, satt i service.kafka1.hostname
  • EXTERNAL://localhost:9094 dirigerer klienter til å spørre mot localhost:9094, siden vi eksponerer 9094 på host-nettverket i services.kafka1.ports
Øvrig konfigurasjon

De mest aktuelle variablene å skru på er antagelig disse:

  • KAFKA_LOG_DIRS definerer 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 peker volumes til samme sti lenger nede.
  • KAFKA_AUTO_CREATE_TOPICS_ENABLE kan 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: 1 setter 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_ID er den unike identifikatoren for noden i clusteret
  • KAFKA_LISTENER_SECURITY_PROTOCOL_MAP definerer sikkerhetsprotokoller. For å holde dette eksempelet enkelt bruker vi PLAINTEXT på alt.
  • KAFKA_INTER_BROKER_LISTENER_NAME og KAFKA_CONTROLLER_LISTENER_NAME definerer 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_ID definerer hvilket cluster denne brokeren tilhører. Denne verdien kan genereres med bin/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:8085

Vi 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.

Liker du innlegget?

Del gjerne med kollegaer og venner