Alt bør automatiseres

I litt for mange prosjekter opplever jeg lange oppskrifter i f.eks. readme bare for å komme i gang. Alt dette burde være så enkelt som å kjøre en kommando.

Ofte må en selv laste uspesifisert versjon av f.eks. Java. Men som jeg skrev i Har du kontroll på ditt utviklingsmiljø? er det veldig viktig at alle, inkludert prod, “er på samme versjon”.

asdf er et flott verktøy for dette, men det er ikke nok. Alle bør forholde seg til de samme kommandoene, om det så er lokal utvikling, bygging, testing eller deployment.

Hva innebærer det egentlig å sette opp lokalt utviklingsmiljø? Ofte må du gjøre følgende:

  • Finne og laste ned riktig versjon av Java/Elixir/Terraform/Node/Deno osv.
  • Definere miljøvariabler nødvendige for å starte appene.
  • Laste ned og kjøre relevante lokale servere, som postgres og redis.
  • Laste ned app-avhengigheter.
  • Starte appen i lokal utviklingsmodus.

Etter dette er du bør du være i stand til å gjøre lokal utvikling. Så kommer bygging og deployment, som gjerne har eget oppsett. Forhåpentligvis er dette scriptet, og har noen vært flinke, brukes eksempelvis make for å binde ting sammen.

Vi er i 2025, det må da finnes en bedre løsning?

La meg vise deg!

mise-en-place (uttales “MEEZ ahn plahs”) er et verktøy som mange har fått øynene opp for. I et nytt prosjekt bestemte jeg meg for å teste det ut – først og fremst for å håndtere avhengigheter, à la asdf, men etter hvert også som et komplett orkestreringsverktøy for hele prosjektet.

Mise gjør 3 ting, de kaller det:

  1. dev tools - håndterer versjoner for deg
  2. environments - de håndterer miljøvariable (og secrets) for deg
  3. tasks - kjører oppgaver og håndterer avhengigheter mellom disse

Punkt 1. er omtrent det asdf gjorde, bare enda raskere og med bedre CLI. Så den hopper vi over. Punkt 2. og 3. er mer interessante.

Tasks

Tasks er bare en liten DSL for å definere en oppgave – som er en viktig brikke for å automatisering. La meg illustrere magien med et eksempel.

[tasks.dev]
depends = ['devBackend', 'devFrontend']
description = "Run app in dev mode"

Fra shellet kan jeg skrive mise run dev (eller bare mise dev om det ikke er noen “konflikt” med andre kommandoer). Denne tasken avhenger av å starte blant annet devBackend og devFrontend.

F.eks. devBackend:

[tasks.devBackend]
depends = ['install', 'dockerComposeUp']
description = "Run backend app in dev mode"
dir = "./backend"
run = "deno run dev"

Denne avhenger igjen av install (deno install) og dockerComposeUp (…ja du gjetta det, docker compose up -d). Disse to taskene kjøres i parallell og når de er ferdige, så kjører mise deno run dev for backend og frontend.

Du ser vel poenget her? Når noen skal starte dette prosjektet, trenger de bare å ha satt opp mise og docker. Og etterpå kjøre mise dev, that’s it. Prosjektet er oppe og kjører.

mise in action

Altså, med en kommando så startes deno dev server, vite dev server og React Cosmos server. Vil påstå det ikke kan bli så mye enklere enn dette.

Deploy

Deployment krever gjerne mange verktøy. I vårt tilfelle, deno for bygg, testing, typecheck, linting osv, docker for pakking og pushing, samt Azure sitt CLI for å faktisk trigge deployment.

Med vårt oppsett kan vi skrive mise testDeploy, og den vil orkestrere alle stegene som skal til. Mise ser hvilke avhengigheter som må kjøres, kjører de i parallell der det er mulig, og tillater deploy, dersom alle tidligere kommandoer fullførte uten feil. Denne tasken ser omtrent sånn her ut:

[tasks.testDeploy]
depends = ['push']
description = "Force deployment in test environment"
run = """
az containerapp update \
  -n app-ca \
  -g test-rg \
  --image theprepacr.azurecr.io/somecr/app:`git rev-parse --short HEAD` \
  --set-env-vars "FORCE_UPDATE=`date +%s`" \
                 "DEPLOYMENT_ENV=test" \
                 "APP_VERSION=`git rev-parse --short HEAD`"
"""

Mise bryr seg ikke om hva den kjører, så dette er et eksempel på et lite script. Dette kan trekkes ut i egne filer om en vil.

Caching

Vi ønsker kun å kjøre en task, dersom den ikke allerede er kjørt. Eksempelvis er det unødvendig å teste backend-kode dersom testene allerede har kjørt vellykket.

[tasks.testBackend]
depends = ['install']
description = "Run test in backend"
env = { DB_URL = "localhost:5433/app" }
dir = "./backend"
sources = ['./src/**', '../deno.lock', '../shared/**/*.ts']
outputs = { auto = true }
run = "deno task test"

De to viktige elementene her er sources og outputs. En kan her angi hvilke inputfiler som trigger re-bygg, samt outputfiler. Eller i dette tilfellet finnes det ingen, så da bruker vi bare { auto = true }.

Andre gang jeg kjører denne kommandoen vil jeg lynkjapt få:

mise testBackend
[init] sources up-to-date, skipping
[install] sources up-to-date, skipping
[testBackend] sources up-to-date, skipping

Env

Mise har førsteklasses støtte for environment. Dette kan spesifiseres globalt og overstyres lokalt for en kommando. Våre apper har en del standardkonfigurasjon basert på hvor de kjører, så da kan vi i mise.toml skrive:

[env]
DEPLOYMENT_ENV = "dev"

Men når det skal kjøre tester, må det overskrives en miljøvariabel som vist i eksempelet over, og da kan jeg bare legge til det i testBackend tasken (vist over):

[tasks.testBackend]
...
env = { DB_URL = "localhost:5433/app" }
...

Mise, verktøyet du kanskje ikke visste du trengte

Da jeg begynte med mise i januar, var det et lite eksperiement, det “var jo bedre enn asdf”. Det tok meg ikke lang tid før jeg forstod at dette verktøyet kunne brukes til mye mer. 3 måneder senere bruker vi det mer og mer. F.eks. bruker vi det til å kjøre terraform, og det er da like enkelt å sette opp “infrastructure as code” prosjektet som app-prosjektet.

Mise er altså et verktøy som kan kjøre tasks, håndtere miljøvariable og versjoner av f.eks. sdk’er. Kombinasjon blir et utrolig kraftig verktøy jeg ikke visste at jeg trengte. Kanskje burde også du prøve?