Skriv portabla appar med Docker Containers

av Johan Lahti

Containerlösningar erbjuder stora fördelar. För oss som jobbar med nätverk är det viktigt att förstå dem, för det påverkar både hur man designar och segmenterar den underliggande infrastrukturen, men också hur man designar applikationer.

”Det funkar på min dator”

”Det funkar på min dator”, hur många gånger har du hört det som svar på en felanmälan? Har du jobbat i IT-svängen ett tag har du garanterat stött på buggar i system som genast kommer att handla om underliggande faktorer kring servern som applikationen körs på.

Genom att istället köra applikationerna i containers kringgår vi dessa bekymmer eftersom vi inte bara paketerar applikationen, utan inkluderar alla dess beroenden. På så sätt kan vi vara helt säkra på att vår kod kommer fungera likadant i utvecklingsmiljön på vår laptop, som i produktion.

Vad är en container?

Kortfattat kan vi jämföra containers med de virtuella maskiner som servat majoriteten av alla workloads på företagen de senaste årtiondena. Med en virtuell server har vi en hypervisor som virtualiserar hårdvara på vilken vi installerar ett operativsystem och allt som hör till driften av den servern. Först när den installationen är klar har vi en plattform där vi kan installera vår applikation.

När vi istället tittar på containers så virtualiserar vi faktiskt ingenting. Istället använder Docker något som heter linux namespaces och control groups för att skapa en container, det vill säga en isolerad miljö som bara har åtkomst till det specifika namespacet. Med hjälp av dessa tekniker kan en container ha helt egna nätverksinterface, egen lagring och vi kan begränsa hur mycket resurser en container får använda från hosten.

Låter nästan som en VM va? Resultat är så nära en VM som möjligt utan att behöva en separat kernel, men mer lightweight, för när det passar sig kan vi faktiskt använda samma binärer och bibliotek för flera instanser av samma container!

Lager på lager

Utifrån en grundimage, skapar Docker ett read-only lager som beskriver de skillnader som görs från grundimagen. För varje steg i byggfasen skapas ett nytt layer ovanpå det föregående och mellanliggande containers som använts för att exekvera steget tas bort igen. När alla stegen i vår build är utförda har vi vårt slutgiltiga lager, containerlagret, där vi kommer starta vår applikation.

Inte nog med att vi får portabilitet, vi kapar bort väldigt mycket overhead. Både operationellt och resursmässigt.

Det här medför så klart ganska stora skillnader både på hur man designar den underliggande infrastrukturen såsom nätverk, men även hur vi designar våra applikationer. Oavsett om du fokuserar på att bygga nät traditionellt, eller har hakat på Devnet-trenden tjänar du alltså på att känna till hur en containermiljö fungerar.

Nu labbar vi!

För att ha något att demonstrera med skriver vi ihop en enkel demo-app. Vi bygger en Pythonapplikation som har ett REST-interface. När vi anropar det här APIet kommer applikationen ansluta mot alla routrar och switchar i min labbmiljö och undersöka alla interface för att leta efter fel, till svar får vi en JSON payload som listar eventuella fel och för vilken enhet det gäller. Om du vill testa stegen på din egen dator kan du ladda ner demoappen här: https://github.com/johan-lahti/demoapp1-healthcheck

Applikationens struktur

Testapplikationen bygger på Flask och Genie och har en struktur som ser ut så här:

.
├── README.md
└── healthcheck
├── devices.yaml <- – – – – –  inventory för labbmiljön
├── startapp.py   <- – – – – – webserver
└── statistics_collector
└── collectors.py <- – – – metod som används för att hämta data

För att starta applikationen behöver vi alltså stå i mappen healthcheck och köra startapp.py, då startas en webserver som lyssnar på port 5000.

Dockerfil

Nu har vi kommit så långt att vi ska beskriva för Docker hur vi vill köra den här applikationen och vad som behövs för att köra den. Vi skriver alla dessa instruktioner i en Dockerfile som vi lägger i samma repository. Här kan man göra en massa trollerikonster, men vi håller oss idag till minimum.

  1. Med kommandot FROM anger vi en grund image att utgå från. Docker kommer skapa ett layer från den här imagen, i det här fallet Ubuntu 20.04
  2. COPY kopierar filer från mappen som Docker klienten står i. I det här fallet innebär det att vi kopierar över alla filer från root-katalogen.
  3. RUN är vanliga linuxkommandon som körs i containern, här kör vi några kommandon för att installera python, PIP och telnet. Det är verktyg vi kommer behöva senare.
  4. Vi kör också RUN för att installera de pythonbibliotek vi behöver med hjälp av pip.
  5. WORKDIR kommandot är motsvarande CD i linux/mac/windows, dvs vi stegar in i katalogen /app/healthcheck.
  6. Det sista steget skapar ett layer där vi kör igång applikationen genom att starta pytonfilen startapp.py

 

Build & Run

Nu när vi fått ihop hela instruktionen för hur vi vill att Docker ska bygga vår container är det dags att göra en build. Jag kör Docker desktop direkt på min Mac och det fungerar likadant om du kör på Windows eller Linux.

Med terminalen går jag till mappen som är root för projektet, där jag nu också har Dockerfilen:

— Codeformat —
.
├── Dockerfile
├── README.md
└── healthcheck
├── devices.yaml
├── startapp.py
└── statistics_collector
└── collectors.py
—end —

Därefter kör jag bara Docker build kommandot:

Docker build .

Nu kommer Docker köra igenom alla stegen i Dockerfilen, för att till slut ge oss en image som vi kan starta för att köra igång vår applikation. Nu har jag kört exakt den här builden innan, så min output kommer visa att Docker använder sin cache för alla steg, men första gången kommer den visa output från exempelvis när den hämtar externa bibliotek från pip.

johanlath@Johans-MacBook-Pro demoapp1-healthcheck % Docker build .
Sending build context to Docker daemon  62.46kB
Step 1/10 : FROM rackspacedot/python37
---> 5a79ecea83c2
Step 2/10 : COPY . /app
---> Using cache
---> c8c38d7f0b4c
Step 3/10 : RUN pip install --upgrade pip
---> Using cache
---> 0c7f76198c25
Step 4/10 : RUN pip install flask
---> Using cache
---> 624ed57cd421
Step 5/10 : RUN pip install genie
---> Using cache
---> 261afad6d68d
Step 6/10 : RUN pip install pyats
---> Using cache
---> 9f4e956c3252
Step 7/10 : WORKDIR /app/healthcheck
---> Using cache
---> bec40044cd71
Step 8/10 : ENTRYPOINT ["python"]
---> Using cache
---> b7346dbe196d
Step 9/10 : CMD ["startapp.py"]
---> Using cache
---> 51f85a247a2b
Step 10/10 : MAINTAINER Johan Lahti
---> Using cache
---> a4fd05cda42a
Successfully built a4fd05cda42a

På sista raden får vi ett image id som vi kan referera till för att starta den här imagen. För att underlätta skulle vi kunna köra build med flaggan ”-t” för att ge imagen en förutbestämd tag:

johanlath@Johans-MacBook-Pro demoapp1-healthcheck % Docker build -t test .
…
Step 10/10 : MAINTAINER Johan Lahti
---> Using cache
---> a4fd05cda42a
Successfully built a4fd05cda42a
Successfully tagged test:latest

När builden är klar kan vi helt enkelt starta en container med den specifika imagen genom att köra ”run”:

Docker run test

Extern konnektivitet?

Någonting fattas fortfarande, när jag anropar REST-API:t så får jag inget svar. Det beror på att vår container fortfarande körs helt isolerad från omvärlden.

Det löser vi genom att när vi startar containern lägger med flaggan ”-p”

Docker run -p 127.0.0.1:80:5000 test

Med den flaggan talar vi om för Docker att mappa 127.0.0.1:80 till containerns port 5000, på så vis kan vi nå vår applikation genom att ansluta mot 127.0.0.1 på port 80, och Docker ser till att trafiken kommer fram.

Ny approach men samma grund återstår

Vid det här laget har du förstått att skillnaden att köra containers jämfört med virtuella servrar är att vi kör många applikationer på samma server, vi har inte längre en unik IP adress per instans på samma sätt, istället får vi tala om för Docker hur vi ska nå fram till rätt container.

Detsamma gäller åt andra hållet, vi kan inte längre lita på en IP som identitet för en specifik applikation i brandväggarna utan trafiken kommer att komma från en delad IP, eller i produktionsmiljöer, en slumpmässig IP från ett stort nät.

Att tänka på

Nu när vi förstår hur containerlösningar fungerar ser vi snabbt hur det förenklar vägen till ett DevOps arbetssätt. Vi kan också enklare relatera till trycket att gå mot multicloudlösningar som ger instant-scale möjligheter i publika moln utan att lämna tryggheten med att köra workloads on-premise.

Vi inser också snabbt att vi behöver en ny approach för att hantera säkerheten i näten. Vi som jobbar med nätverk måste hitta en annan approach för att hantera segmentering när företagen kör mer och mer containers eller serverless. En bra ände att börja i är att titta på datacentersidan, kör ni Cisco ACI finns möjlighet att integrera med Kubernetes och göra segmenteringen enkel. På så vis kan du få varje kubernetes namespace att mappas till en egen EPG i ACI. Och det där med multicloud? Vi kanske ses vid ritbordet!

Bild: jg-marczak/Unsplash

Kontakta oss!
Svar inom 24h