Kubernetes

Konfiguration, hemligheter och hälsokontroller

Samma image i alla miljöer — och hur Kubernetes avgör om en pod mår bra

Hoppa till

Var vi är

Trafiken hittar in i klustret och poddarna svarar. Kvar finns två frågor som förra delen lämnade orörda, och båda handlar om vad som händer inuti en pod.

Den första: hur kör man samma applikation i test och produktion när inställningarna skiljer sig? Den andra: introduktionen nämnde att Kubernetes “frågar löpande varje pod om den mår bra” — men vad frågar den, och vad händer vid ett nej?

Varför konfiguration inte hör hemma i imagen

Lisa behöver köra API:et mot testdatabasen i test och mot den riktiga i produktion. Adressen ligger i en miljövariabel som bakas in vid bygget, så hon bygger två images: searchapi:v3 och searchapi:v3-test.

Det fungerar i en vecka. Sedan upptäcker teamet en bugg i produktion som inte går att återskapa i test, och ingen kan längre säga säkert om de två imagerna innehåller exakt samma kod.

Regeln som följer

Poängen med en image är att den ska vara testad en gång och körd överallt. Bygger du en image per miljö har du inte längre testat det som körs i produktion — du har testat något som liknar det.

Imagen innehåller applikationen, miljön levererar inställningarna. Samma searchapi:v3 går hela vägen från test till produktion, och det enda som skiljer är vad den matas med vid start.

searchapi:v3 test stage prod db=test db=stage db=prod ConfigMap per miljö
Samma låda till vänster, tre olika lappar till höger.

Vad en ConfigMap är

En ConfigMap är ett objekt i klustret som håller inställningar som nyckel/värde-par.

apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
data:
  LOG_LEVEL: "info"
  FEATURE_SEARCH: "true"
  DB_HOST: "db"

Filen är vanlig YAML och hör hemma i Git, granskad som all annan kod. Värdena står i klartext, eftersom de inte är hemliga.

En ConfigMap har en storleksgräns på ungefär en megabyte. Behöver du mer är det inte konfiguration längre utan data, och hör hemma någon annanstans.

Vad en Secret är

En Secret är samma sorts objekt, avsett för värden som inte ska stå i klartext.

apiVersion: v1
kind: Secret
metadata:
  name: api-secrets
type: Opaque
stringData:
  DB_PASSWORD: "hemligt"

type: Opaque betyder “godtyckliga nyckel/värde-par” och är det vanliga. Det finns särskilda typer för TLS-certifikat och registry-inloggningar, men de fungerar likadant.

ConfigMap vs Secret

De två objekten är förvånansvärt lika, och skillnaden är mindre än namnen antyder.

En ConfigMap lagrar värdet som du skrev det. Den är avsedd för sådant som får synas i en merge request: loggnivåer, funktionsflaggor, värdnamn.

En Secret lagrar värdet base64-kodat och markeras som känslig. kubectl get secret api-secrets visar en sammanfattning, inte värdena.

Ber du om YAML eller JSON syns de base64-kodade värdena. En Secret markerar känslighet, men döljer inte värdet för någon som får läsa objektet.

Båda kan lämnas till containern som miljövariabler eller monterade filer. Men manifest, åtkomst och hantering av känsliga värden skiljer sig, så byt inte bara kind: ConfigMap mot kind: Secret.

stringData vs data

Två fält i samma Secret som gör samma sak från olika håll.

stringData låter dig skriva värdet i klartext. Kubernetes kodar om det till base64 när objektet sparas.

data kräver att du själv anger värdet redan base64-kodat.

Läser du tillbaka en Secret ser du alltid datastringData finns bara vid skrivning och försvinner ur objektet. Det är därför en Secret du precis skapat “inte ser ut som” filen du skickade in.

En Secret är inte krypterad

Fallgrop

Att värdet blir base64 är lätt att tolka som säkerhet. Det är det inte — base64 är kodning, inte kryptering, och vem som helst med läsrättighet kan avkoda värdet på en sekund:

kubectl get secret api-secrets \
  -o jsonpath='{.data.DB_PASSWORD}' | base64 -d

Som standard lagras Secrets dessutom okrypterade i klustrets databas.

Det som faktiskt skyddar dem är tre saker: behörigheter (RBAC), att kryptering i vila slås på av den som driftar klustret, och att YAML-filen med hemligheten aldrig committas.

Skillnaden mot en ConfigMap är alltså i huvudsak att en Secret markerar avsikten och hanteras varsammare av verktygen — inte att den är säker i sig.

Vill du inte lägga hemligheter i klustret alls är nästa steg att hämta dem från en extern valvtjänst vid start. Det ligger utanför den här serien, men det är dit resan går i de flesta team.

Hur värdena når containern

I Deploymenten hämtas båda objekten med envFrom, som gör varje nyckel till en miljövariabel:

    spec:
      containers:
        - name: api
          image: registry.example.com/searchapi:v3
          envFrom:
            - configMapRef:
                name: api-config
            - secretRef:
                name: api-secrets

Nu har containern LOG_LEVEL, FEATURE_SEARCH, DB_HOST och DB_PASSWORD som helt vanliga miljövariabler. Applikationskoden behöver inte ändras en rad, och vet ingenting om Kubernetes.

Vill du bara ha en enskild nyckel finns env med valueFrom i stället för envFrom.

Miljövariabel vs monterad fil

Det finns två sätt att leverera värdena, och valet får en konsekvens som förvånar.

Som miljövariabler är enklast. Applikationen läser dem som vanligt, och det fungerar i varje språk utan bibliotek.

Som monterad fil lägger Kubernetes värdena som filer i en katalog i containern. Applikationen läser filen i stället.

Skillnaden märks vid ändring: miljövariabler sätts en gång vid start och läses aldrig om. En ConfigMap eller Secret som monteras som vanlig volym uppdateras efter en stund, men applikationen måste läsa om filen.

Monteras filen med subPath uppdateras den inte automatiskt.

Miljövariabler är enklare. Monterade filer är mer flexibla, och nödvändiga för värden som är för stora eller för strukturerade för en variabel, som ett certifikat.

LOG_LEVEL=info /cfg/log=info LOG_LEVEL=info /cfg/log=debug env fil env fil ConfigMap ändras vid start efteråt
Samma container två gånger. Bara den nedre raden har bytt värde.

När ändringen slår igenom

Ändrar du en ConfigMap händer ingenting med poddar som redan kör.

De fick sina miljövariabler när de startade och fortsätter med de gamla värdena tills de byts ut. Ingen varning, inget felmeddelande — bara ett värde som envist är kvar.

kubectl rollout restart deployment/api

Kommandot startar en ny utrullning. Hur många poddar som byts samtidigt beror på Deploymentens strategi. De nya poddarna plockar upp de nya värdena när de startar.

Vad en probe är

En probe är en kontroll Kubernetes kör mot en container med jämna mellanrum för att avgöra hur den mår.

Kontrollen är oftast ett HTTP-anrop mot en sökväg du bestämmer, men kan också vara ett kommando som körs inuti containern eller en TCP-anslutning mot en port.

Det finns tre sorter, och de gör helt olika saker vid ett nej. Att blanda ihop dem är den vanligaste allvarliga missen i Kubernetes-konfiguration.

Vad livenessProbe gör

livenessProbe frågar: är den här containern bortom räddning?

Vid nej startas containern om. Podden behåller sitt namn, men processen inuti dödas och startas på nytt.

Sonden ska därför bara svara på något den egna processen rår över — att den inte hängt sig, låst sig eller slutat svara.

Vad readinessProbe gör

readinessProbe frågar: kan den här podden ta emot ett anrop just nu?

Vid nej tas podden ur trafik men får leva. Servicen slutar skicka anrop till den, och börjar igen så fort svaret blir ja.

Här är det däremot rimligt att ta hänsyn till beroenden. En pod som tappat kontakten med databasen kan gott sluta ta emot anrop en stund.

Liveness vs readiness

Samma sorts anrop, helt olika konsekvens vid nej.

livenessProbe

Frågar: är den här containern bortom räddning?

Vid nej: containern startas om.

Ska svara på något som bara den egna processen rår över — att den inte hängt sig.

readinessProbe

Frågar: kan den här podden ta emot ett anrop nu?

Vid nej: podden tas ur trafik, men fortsätter köra.

Får gärna ta hänsyn till att beroenden tillfälligt är otillgängliga.

Frågan liknar varandra. Svarets följd gör det inte.

Tumregeln: är det något en omstart skulle laga? Ja → liveness. Nej → readiness.

Vad startupProbe löser

startupProbe finns för applikationer som tar lång tid att komma igång.

Så länge den kör är de andra två pausade. Först när den lyckats tar readiness och liveness vid.

Utan den skulle du behöva ge livenessProbe en lång tolerans, så att en app som startar på tre minuter inte dödas i onödan. Den toleransen skulle sedan gälla för alltid, även när appen verkligen hängt sig.

Sonderna i ordning

startupProbeHar applikationen kommit igång? Ger långsamma appar tid att starta utan att dödas i onödan.när starten är klarreadinessProbeÄr podden redo att ta emot trafik just nu? Nej → den plockas ur Servicen, men får leva.löpande, parallelltlivenessProbeLever processen fortfarande? Nej → containern startas om.
Sonderna tar över efter varandra. Så länge startupProbe kör är de två andra pausade.
          startupProbe:
            httpGet:
              path: /health
              port: 5000
            failureThreshold: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 5000
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 5000
            periodSeconds: 10

startupProbe tillåter här 30 misslyckanden med tio sekunders mellanrum — alltså ungefär fem minuter. Därefter startar kubelet om containern. Det är inte ett permanent stopp; nästa start får ett nytt försök.

Fallgropen: omstartsstormen

Fallgrop

Den vanligaste allvarliga missen: låta livenessProbe peka på en endpoint som kontrollerar databasen.

Blir databasen seg svarar alla poddar nej samtidigt. Kubernetes gör då exakt som den blivit tillsagd och startar om samtliga — vilket ger en anstormning av nya anslutningar mot en databas som redan har det svårt. Systemet går från långsamt till nere, och orsaken var övervakningen.

Regeln: livenessProbe ska bara svara på om processen lever. Beroenden hör hemma i readinessProbe, där konsekvensen är att podden pausas i stället för att dödas.

Vad det här betyder i ditt arbete

Du kommer ofta att använda envFrom och probes i Deployment-manifest. De få raderna påverkar hur systemet beter sig en dålig dag, så välj dem utifrån tjänstens faktiska behov.

Vid felsökning kan en ändrad ConfigMap som inte slagit igenom kräva en ny utrullning. En pod som startar om i loop kan också bero på en felinställd livenessProbe, men kontrollera poddens händelser och loggar innan du väljer åtgärd.

RBAC, kryptering i vila och externa valvtjänster blir dina den dag du äger klustret. Det som inte kan vänta är att veta att en Secret inte är krypterad, för det påverkar vad du får lägga i den redan i dag.

Vanliga nybörjarförväxlingar

En Secret är inte krypterad. Base64 är kodning, och vem som helst med läsrätt kan avkoda den.

ConfigMap och Secret når containern på samma sätt. Skillnaden är avsikt och hur verktygen och behörigheterna hanterar dem.

stringData finns inte kvar i objektet. Läser du tillbaka Secreten ser du data, base64-kodat.

En ändrad ConfigMap uppdaterar inte körande poddar som läser den som miljövariabler. De sätts vid start.

livenessProbe och readinessProbe är inte utbytbara. Den ena startar om, den andra tar ur trafik.

En livenessProbe som kontrollerar databasen är inte grundlig. Den är en risk.

Prova själv

Det här kräver inget kluster. --dry-run=client betyder att kubectl bygger objektet lokalt och skriver ut det utan att kontakta något.

Skapa först en ConfigMap och titta på resultatet:

kubectl create configmap api-config \
  --from-literal=LOG_LEVEL=info \
  --dry-run=client -o yaml

Under data: står LOG_LEVEL: info — precis som du skrev det. Gör nu samma sak som en Secret:

kubectl create secret generic api-secrets \
  --from-literal=DB_PASSWORD=hemligt \
  --dry-run=client -o yaml

Nu står det DB_PASSWORD: aGVtbGlndA==. Värdet ser skyddat ut. Avkoda det:

echo "aGVtbGlndA==" | base64 -d

Du får tillbaka hemligt. Det tog ett kommando och ingen nyckel — vilket är hela poängen med avsnittet om att en Secret inte är krypterad.

Lägg märke till att de två utskrifterna är nästan identiska. Enda skillnaden är kind och att värdet kodats om. Ingen av dem är skyddad av sig själv.

Tre saker som kan förvirra: --dry-run=client skickar ingenting till ett kluster, så kommandot fungerar även om kubectl get pods skulle klaga på att den inte når någon server. Utskriften saknar stringData trots att du angav ett klartextvärde — det fältet finns bara vid skrivning. Och på äldre macOS heter avkodningsflaggan -D i stället för -d.

Kom ihåg
  • Konfiguration hör inte hemma i imagen. Samma image ska gå från test till produktion.
  • ConfigMap för vanliga inställningar, Secret för känsliga — men base64 är inte kryptering.
  • stringData skrivs i klartext och lagras som data i base64.
  • Miljövariabler sätts vid start. En ändrad ConfigMap som läses som miljövariabler kräver kubectl rollout restart.
  • Monterade filer uppdateras efter en stund, men appen måste läsa om dem. subPath uppdateras inte.
  • livenessProbe startar om, readinessProbe tar ur trafik, startupProbe ger starten tid.
  • Låt aldrig livenessProbe bero på en databas — det förvandlar en långsam dag till ett avbrott.
Kolla att du hänger med
Varför är det ett problem att bygga en image per miljö?
Då är det inte längre samma artefakt som testats och som körs i produktion. Buggar kan uppstå i skillnaden mellan imagerna, och ingen kan säkert säga att de innehåller samma kod. Bygg en image och skjut in inställningarna vid start.
En kollega säger att lösenordet är säkert eftersom det ligger i en Secret. Har hen rätt?
Nej. Värdet är base64-kodat, inte krypterat, och avkodas med ett kommando av vem som helst som får läsa objektet. Det som skyddar är behörigheter, kryptering i vila på klustret, och att filen aldrig committas.
Du ändrar ett värde i en ConfigMap. Poddarna fortsätter använda det gamla. Varför?
Miljövariabler sätts när containern startar och läses aldrig om. Kör kubectl rollout restart deployment/api så startar en ny utrullning och nya poddar läser värdet. Monterade filer uppdateras efter en stund, men applikationen måste läsa om dem. subPath uppdateras inte.
Vad är den praktiska skillnaden mellan livenessProbe och readinessProbe?
Konsekvensen vid nej. Ett nej från liveness startar om containern; ett nej från readiness tar bara ut podden ur Servicen så den slutar få trafik, medan den fortsätter köra. Tumregeln: skulle en omstart laga det? Ja → liveness.
Din applikation tar tre minuter att starta. Varför är startupProbe bättre än att bara ge livenessProbe lång tolerans?
En lång tolerans i liveness gäller för alltid — även när appen verkligen hängt sig, och då dröjer omstarten i minuter. startupProbe pausar de andra sonderna under starten och lämnar sedan över, så att liveness kan vara strikt i normaldrift.
Varför ska livenessProbe inte kontrollera databasanslutningen?
För att en seg databas då får alla poddar att svara nej samtidigt, varpå Kubernetes startar om allihop. Omstarterna öppnar en våg av nya anslutningar mot en redan pressad databas. Kontrollera beroenden i readiness i stället, där podden pausas i stället för att dödas.

Snabb sammanfattning

  • Imagen bär applikationen, klustret bär inställningarna. En image genom alla miljöer.
  • En ConfigMap håller inställningar som nyckel/värde-par, i klartext, i Git.
  • En Secret är samma sak för känsliga värden, lagrade base64-kodade.
  • De når containern på samma sätt. Skillnaden är avsikt och hur verktygen och behörigheterna hanterar dem.
  • stringData skrivs i klartext och sparas som data i base64. Fältet finns inte vid läsning.
  • Base64 är kodning, inte kryptering. RBAC, kryptering i vila och .gitignore är skyddet.
  • envFrom gör varje nyckel till en miljövariabel. env med valueFrom för en enskild.
  • Miljövariabler sätts vid start. Monterade filer uppdateras men måste läsas om.
  • En ändrad ConfigMap som läses som miljövariabler kräver kubectl rollout restart för att slå igenom.
  • En probe är en återkommande kontroll: HTTP-anrop, kommando eller TCP-anslutning.
  • Liveness startar om, readiness tar ur trafik, startup ger starten tid och pausar de andra.
  • En livenessProbe som kontrollerar beroenden förvandlar en långsam dag till ett avbrott.

Nästa del

Nu vet Kubernetes vad podden ska matas med och om den mår bra. En sak till behöver klustret veta innan podden är fullt beskriven: hur mycket den får ta.

Nästa del handlar om resurser — hur mycket minne och processorkraft en pod får begära, och vad som händer när den tar mer. Svaret är olika för de två, och skillnaden avgör om podden blir långsam eller dör.