Alt på sin plass med mise og compose 🧑🏻‍🍳

Elisabeth Irgens og Robin Kåveland

Du kloner et repo, ivrig etter å hive deg rundt og bli kjent med applikasjonen. Men hvordan i svarte er det egentlig man kjører opp denne greien lokalt? Er det meningen jeg skal vite dette? Problemet er en rotete kjøkkenskuff med det rare i, at det er ukjent hvilke redskap som fins og hva du trenger. Kjedelig! Vi har gjort en innsats for å gjøre det enkelt og moro å sette i gang med kokkelering. Kjøkkenbenken har blitt så ryddig at vi har lyst dele hvordan vi har lagt ut alt på sin plass — preppet og klart til bruk.

Illustrasjon med forvilet utvikler som ikke finner ut av hvorfor bygget feiler

Det funker på din maskin, men ikke på min

Vi har 528 aktive repo i Amedia sin GitHub-organisasjon. Noen får mye kjærlighet, noen ble opprettet forrige uke basert på en vedlikeholdt felles template. Mens andre repo… de kan ligge bortgjemt i en krok så lenge at det har gått et tiår siden noen jobbet med dem aktivt. Teamet vårt har arvet 10 repo av den siste typen. Gammelt skjørt arvegods. Jeg (Elisabeth) har pirket forsiktig på systemet, fått et par av dem til å våkne til liv, funnet ut hvordan gjøre endringer i alle uten at de ramler sammen. Men jeg var langt unna å ha noe som lignet et brukbart lokalt utviklingsmiljø. Robin joinet meg i oktober og da var vi klar for tyngre løft i denne porteføljen av programvare. Java 8 skulle bli til minst 21 overalt. Fint om vi kunne lykkes med å slette noe kode også. Når vi skal herje masse med 10-15 repo, så måtte vi få kontroll over avhengigheter, miljøvariabler og kommandoer som trengs i de ulike applikasjonene. Vi trengte at det skulle bli enkelt for oss begge å hoppe mellom alle disse repoene.

Illustrasjon av lang readme som fører til en table flip

README-filene som ingen orker å lese

Hva med dokumentasjon i README? Problemet er at det er ingen grunn til å stole på at den er oppdatert, og all grunn til å tro at den er full av løgn. De lengste er verst. Spesielt med veldig lange READMEs har det typisk vært vanskelig for andre som kommer etterpå å vite hva som er relevant, derfor blir heller ikke detaljer oppdatert. Robin forteller at han hopper over å lese README-filer til fordel for å lese byggeskript. Skulle ønske jeg hadde hørt dette tipset før, for jeg har brukt mye tid på å gå meg vill i omfattende halvsannheter fra README-filer. Selv om dokumentasjon faktisk er oppdatert, så betyr ikke det er enkelt å følge krumspringene i en oppskrift for hvordan komme i gang. Antagelser om kjennskap til ulike teknologier, antagelser om avhengigheter. Her er kommandoen for noe helt basic som du allerede kjenner, men lykke til med å sette opp en lokal Postgres og å koble opp noe Kafka.

Fins det ikke en bedre måte å preppe lokalt utviklingsmiljø?

To glade utviklere med fungerende lokalt utviklingsmiljø

Oppsett av utviklingsmiljø med mise og compose

Et veldig godt alternativ til en utfyllende README er å automatisere så mye som mulig av oppsett, og sørge for at det automatiserte oppsettet er i bruk. Nettopp dette er grunnen til at Robin liker å lese byggescripts heller enn README-filer -- at de er i bruk gjør at de sannsynligvis er oppdatert og korrekt. En README-fil brukes kanskje bare én gang per utvikler, for å sette opp utviklingsmiljø lokalt. I et lite team som vårt, med omtrent 15 git repositories og 2 utviklere er det svært usannsynlig at noen har brukt README i det siste.

Vi tror løsningen er å flytte informasjon om oppsett til et sted hvor det vil bli brukt ofte og automatisk. Til dette bruket liker vi mise veldig godt. Heller enn å dokumentere hvilke versjoner av hvilke språk og hvilke hendige verktøy vi trenger i README, så legger vi dem inn i mise.toml, for eksempel:

[tools]
java = "corretto-21.0.9.10.1"
maven = "3.9.12"
trivy = "latest"

Så overlater vi oppsett av utviklingsmiljøet til mise, slik at vi vet at dette er i bruk. Da har vi mye bedre grunn til å stole på at inneholdet er riktig.

Svært ofte trenger vi også infrastruktur for lokal utvikling, for eksempel en database eller søkemotor. Der tror vi den enkleste veien til mål er å bruke compose-filer og containere. Da blir det forholdvis enkelt å sette opp infrastruktur uten å måtte forholde seg til mange detaljer om maskinen utviklingsmiljøet skal kjøre på, og så fungerer filen også fint som dokumentasjon av hva applikasjonen trenger for å fungere.

For å hekte compose.yml sammen med mise.toml så liker vi godt å lære applikasjonene våre å motta konfigurasjon fra miljøvariabler, slik at vi også får dokumentert lokalt oppsett av applikasjonen i mise.toml på en kjørbar måte:

[env]
API_SERVER_ROLE="dev"
# defined in compose.yml
OPENSEARCH_URL="https://localhost:9200"
SMTP_SERVER="localhost"
SMTP_PORT="1025"

Garantien for at dette holdes oppdatert får vi gjennom å sørge for at det blir brukt. Der kan vi bruke mise tasks til å gjøre det dumt å la være!

Her er et eksempel på noen hendige tasks for å starte frontier lokalt.

[tasks.compose-up]
run = "podman compose up -d"

[tasks.build]
run = "mvn clean install"

[tasks.start]
depends = ["compose-up", "build"]
run = "mvn spring-boot:run"

[tasks.image]
depends = "build"
run = "podman build -t frontier:local ."

[tasks.trivy]
depends = "image"
run = "trivy image frontier:local"

Dette er en spring-boot applikasjon vi bygger med maven, men så lenge vi lager fornuftige tasks for å jobbe med den, så trenger vi ikke huske nøyaktig hvordan det gjøres, og vi kan jo gjerne gjenbruke navn for tasks i andre applikasjoner som bruker andre rammeverk og språk. Da kan vi forholde oss til stort sett de samme kommandoene i flere repositories. Dersom vi trenger å huske detaljene, så står de jo der i mise.toml også, så denne abstraksjonen er ganske gjennomtrengelig.

Det vi prøver å oppnå er at mise run start blir minste motstands vei. Så lenge disse jobbene vi har definert i mise.toml sparer oss for en del arbeid, så er vi sikre på at vi kommer til å holde dem i god stand.

Bonustips

En liten optimalisering:

[tasks.build]
# Bare hvis pom.xml eller noe under src/ har blitt endret siden vi bygget sist
sources = ['pom.xml', 'src/**']
outputs = 'target/frontier-*.jar'
run = "mvn clean install"

Kombinasjonen mise og compose kan gjøre ganske tunge løft for å gjøre det smidig å få opp lokale utviklingsmiljø.

mailpit

Noen av de eldre applikasjonene våre har ikke lyst til å starte med mindre de får koble seg opp mot en SMTP-tjener slik at de kan sende epost (vi trodde heller ikke det var sant først).

Da vi fikk det behovet, så oppdaget vi mailpit - den kan konfigureres i compose.yml slik som dette:

services:
  mailpit:
    image: axllent/mailpit
    ports:
      - "127.0.0.1:1025:1025"
      - "127.0.0.1:8025:8025"

Da får du en SMTP-tjener på port 1025 på maskinen din, og muligheten til å studere de vakre epostene til applikasjonen din i et webui på [http://localhost:8025].

SQL-script

Dersom applikasjonen din trenger en relasjonsdatabase, så er det hendig å vite at mange images av slike kan kjøre gjennom en katalog med SQL-script når de blir opprettet. Her er "det vanlige" oppsettet av postgres i compose.yml, for eksempel:

services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    command: postgres -c shared_preload_libraries=pg_stat_statements,auto_explain
    ports:
      - '127.0.0.1:25432:5432'
    volumes:
      - pg_data:/var/lib/postgresql
      - ./db-initscripts:/docker-entrypoint-initdb.d
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=postgres
volumes:
  pg_data:

Når du kjører compose up med denne, så vil den spole gjennom SQL-filene som finnes i ./db-initscripts i repoet i alfanumerisk rekkefølge før den starter første gangen. Et hendig sted å opprette databaser, databasebrukere og testdata, om man har behov for slikt.

Kompliserte mise-tasks

mise gir oss også friheten til å programmere tasks i masse forskjellige språk - vi kan legge en kjørbar fil i mise-tasks/task-navn, lage en helt normal unix shebang og programmere i det språket vi foretrekker. Vi har tilgang til alle verktøyene som mise installerer for oss fra [tools]. Dette er veldig hendig om vi skal gjøre litt mer kompliserte ting enn det som får plass på én linje, for eksempel å styre flere prossesser og passe på at man rydder opp etter seg.

Slike tasks kalles file-tasks og støtter alt det samme av parametre som de enklere variantene som lever i mise.toml.

Kort om uttale

Navnet mise kommer fra det franske kulinariske begrepet mise en place og Wikipedia beskriver at det er et uttrykk:

som betyr at alt er på plass, og innen gastromien betyr dette at alle ingredienser, redskaper og så videre er beredt slik at det bare er for kokken å sette i gang.

Derfor uttales mise med kort i, som i ordet spis, ikke med diftong som i ordet mais. Det gjør at det er gode sjanser for at mise-kommandoer blir morsomme når man sier dem høyt til hverandre: ikke "mise trust", men "mistrust", ikke "mise use" men "misuse".

Misuse java? Yes, chef!