Det har aldrig varit så enkelt att bygga lastbalanserade lösningar som nu med hjälp av Docker Swarm. Ännu smidigare blir det med Docker Stack och en enkel docker-compose.yml-fil. Här visar jag ett enkelt exempel på hur man kan bygga sin egen lastbalanserade Docker registry med tre noder på internet.

En val

Docker Swarm sköter lastbalanseringen själv mellan noderna. För att vi även ska få DNS:en att automatiskt ta bort noder som ligger nere använder jag AWS Route53.

För att det ska vara möjligt att ha ett gemensamt lagringsutrymme för alla Docker registries i svärmen väljer jag att använda AWS S3 för lagringen.

Denna labben går att applicera på andra tjänster än bara Docker registry. Här har jag valt en registry för att det var smidigt att laborera med.

Förberedelser

Route53 / DNS

Här förutsätter jag att du redan har din domäns DNS hos Route53.

Börja med att skapa en Health Check för varje nod i Route53. Välj HTTPS som check, och ange IP-adresserna till dina noder. Eftersom vi ännu inte satt upp vår Docker registry-tjänst kommer Health Checkarna visa att noderna är nere. Men detta löser sig när vi fått upp vår Docker registry.

Steg två är att skapa tre A-records för din tänkta subdomän som du vill använda för din Docker registry. Jag väljer här registry.labs.cyberinfo.se. I den första A-recorden skriver du in IP-adressen för den första noden, nod1. Under Routing Policy väljer du Weighted. Under Weight skriver du 1, och under Set ID skriver du nod1. Under Associate with Health Check väljer du Yes, och väljer sen den Health Check som är för just denna noden.

Sätt TTL till 60 sekunder för att inte andra DNS:er ska cacha IP-adressen för länge (vi vill att felande noder snabbt ska plockas bort).

ANNONS FÖR VÅRA EGNA BÖCKER Demonerna på internet

Repetera stegen för de andra två noderna, men skriv istället in de nodernas IP-adress och Set ID sätter du till nod2 och nod3. Välj samma Weight för alla, det vill säga 1. Detta gör att alla noderna kommer få ungefär lika mycket trafik. Internt kommer svärmen också att distribuera lasten jämnt över noderna.

Skapa en IAM-policy, en IAM-användare och en S3-bucket på Amazon AWS

Innan vi börjar med Docker kan vi förbereda en S3-bucket och en användare för den. Vi behöver även skapa en policy som bara har åtkomst till just den bucketen.

Jag skapar först en S3-bucket som jag för detta ändamålet döper till my-registry-bucket.

Därefter skapar jag en policy som ser ut enligt nedan och döper den till registry-lab.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::my-registry-bucket",
                "arn:aws:s3:::my-registry-bucket/*"
            ]
        }
    ]
}

Skapa därefter en ny IAM-användare och välj alternativet Programmatic access. Koppla den nya användaren till policyn du skapade ovan. Spara sedan din Access Key och Secret Key som du får.

Installera Docker

Först och främst behöver vi installera Docker på noderna om detta inte är redan är gjort. Hur man gör detta skiljer sig lite från system till system. Installationsanvisningar för de flesta Linuxdistributioner finns på Dockers webbsida.

Aktivera Docker Swarm

Som standard är inte Docker Swarm aktiverat. Här initialiserar jag Docker Swarm på den första noden, nod1. Denna nod blir automatiskt ledaren.

root@nod1~#> docker swarm init
Swarm initialized: current node (w8gg0fzdr6po7gqq3cu58bqtp) is now a
manager.
...

Jag vill att nod2 och nod3 ansluter sig till svärmen som managers (eftersom vi bara har tre noder). Jag skriver därför följande kommando på nod1.

root@nod1~#> docker swarm join-token manager
To add a manager to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-45p8k8bh6... 81.4.110.59:2377

Klistra in hela kommandot i nod2 och nod3 och kör det. Då kommer noderna att ansluta sig som managers till svärmen.

root@nod2~#> docker swarm join --token SWMTKN-1-45p8k8bh6... 81.4.110.59:2377
This node joined a swarm as a manager.

root@nod3~#> docker swarm join --token SWMTKN-1-45p8k8bh6... 81.4.110.59:2377
This node joined a swarm as a manager.

Certifikat

För att det ska gå att använda en Docker registry över ett nätverk eller över internet krävs det att anslutningen är säkrad med TLS. Det går även utan – om man kör i insecure mode – men det är inte rekommenderat.

Här utgår jag från att du redan har ett giltigt certifikat för din domän. Placera dina certifikat på nod1. Jag har döpt mitt certifikat till cert.crt och nyckeln till cert.key. För att vi ska kunna använda certifikaten på alla noderna – utan att själva behöva distribuera dem – lägger vi till dem som secrets i Docker.

root@nod1~#> cat cert.key | docker secret create key -
ggyr7tiqk0aygk1225yt4ilzz
root@nod1~#> cat cert.crt | docker secret create cert -
yenfbit23ulu69soz211bxlcn

Om du vill kontrollera att det fungerade kan du köra docker secret ls. Du ska nu se två stycken secrets i listan. Det ska gå att se dem med docker secret ls på samtliga noder.

Autentisering

Vi behöver också aktivera autentisering, annars hade vem som helst på internet kunnat hämta och ladda upp images till din Docker registry. För detta använder vi det vanliga verktyget htpasswd. Denna finns redan i Docker-imagen för Docker registry och vi kan därför använda den för att generera en lösenordsfil. Kör följande på nod1, men byt ut användarnamnet och lösenordet till något du själv väljer.

root@nod1~#> docker run --entrypoint htpasswd registry:2 -Bbn jackbenny hEmLiGt123 > htpass

Därefter ska vi lägga till lösenordsfilens innehåll som en secret i Docker. Detta gör vi med:

root@nod1~#> cat htpass | docker secret create htpass -

Återigen kan du kontrollera alla secrets med docker secret ls.

Skapa compose-filen

Nu har det blivit dags att skapa docker-compose.yml-filen som vi sedan ska använda för att ta upp hela miljön. Det du behöver ändra på nedan är S3-fälten så att de matchar din användare, key, bucket och region. Du bör också ändra på HTTP_SECRET till något annat än mitt enkla exempel. Detta kan vara i princip vara vad som helst, men bör vara något långt och slumpmässigt.

I denna labb använder jag tre noder. Jag har därför specificerat replicas: 3 i filen. Detta gör att Docker Stack kommer att spinna upp tre stycken Docker registries.

Management-trafiken mellan noderna i svärmen är krypterad som standard. Men för att även den vanliga trafiken mellan noderna ska vara krypterad (över overlay-nätverket) specificerar jag nätverket manuellt i filen. Här aktiverar jag kryptering med encrypted. Då aktiveras IPSec över overlay-nätverket.

version: "3.8"

services:
  my-registry:
    image: registry:2
    ports:
      - 443:5000
    deploy:
      replicas: 3

    environment:
      - REGISTRY_STORAGE=s3
      - REGISTRY_STORAGE_S3_ACCESSKEY=AKIASB...
      - REGISTRY_STORAGE_S3_SECRETKEY=UyefwN...
      - REGISTRY_STORAGE_S3_REGION=eu-central-1
      - REGISTRY_STORAGE_S3_BUCKET=my-registry-bucket
      - REGISTRY_AUTH=htpasswd
      - REGISTRY_AUTH_HTPASSWD_PATH=/var/run/secrets/htpass
      - REGISTRY_AUTH_HTPASSWD_REALM=My-Registry
      - REGISTRY_HTTP_TLS_CERTIFICATE=/var/run/secrets/cert
      - REGISTRY_HTTP_TLS_KEY=/var/run/secrets/key
      - REGISTRY_HTTP_SECRET=myOwnTestingSecret
    networks:
      - my-registry
    secrets:
      - htpass
      - cert
      - key

secrets:
  htpass:
    external: true
  cert:
    external: true
  key:
    external: true

networks:
  my-registry:
    driver: overlay
    driver_opts:
      encrypted: ""

Starta upp allting

Nu har det blivit dags att starta upp allting.

På nod1 ger vi nu följande kommando:

root@nod1~#> docker stack deploy registry -c docker-compose.yml
Creating network registry_my-registry
Creating service registry_my-registry

Nu kan vi kontrollera så att allt ser rätt ut i svärmen med följande kommandon:

root@nod1~#> docker node ls
ID                            HOSTNAME    STATUS       AVAILABILITY        MANAGER STATUS      ENGINE VERSION
w8gg0fzdr6po7gqq3cu58bqtp *   nod1        Ready        Active              Leader              19.03.8
sa4kez0jkcs8g00s2dpvuxjvy     nod2        Ready        Active              Reachable           19.03.8
vogsn1bh3tg95x41hncygnkvf     nod3        Ready        Active              Reachable           19.03.8

root@nod1~#> docker stack ls
NAME                SERVICES            ORCHESTRATOR
registry            1                   Swarm

root@nod1~#> docker stack ps registry
ID             NAME                    IMAGE        NODE    DESIRED STATE    CURRENT STATE           ERROR    PORTS
iydz1a6r4yi6   registry_my-registry.1  registry:2   nod2    Running          Running 26 seconds ago
rxr4qdy17b0f   registry_my-registry.2  registry:2   nod1    Running          Running 26 seconds ago
i9unuiuvw0ib   registry_my-registry.3  registry:2   nod3    Running          Running 26 seconds ago

Allting ser således rätt ut.

Testa att allt fungerar

Nu har det blivit hög tid att testa att allt fungerar. Först och främst måste vi logga in på vår nya Docker registry. I mitt fall med mitt domännamn ser det ut som nedan.

jake@debian10:~$ docker login registry.labs.cyberinfo.se
Username: jackbenny
Password:

Login Succeeded

Inloggningen lyckades och vi kan gå vidare.

Om du skulle få ett felmeddelande om 503: Service unavailable så testa att skapa en tom fil i din S3-bucket. Filnamnet spelar ingen roll. Det är en bugg som jag sett att många har haft, inklusive jag själv.

För att testa att både ladda upp och ner en image från den egna Docker registryn, hämtar jag först hem en vanlig Ubuntu-image. Denna kommer jag sedan att ladda upp till min egna Docker registry. Därefter tar jag bort den lokalt från datorn, och laddar sen ner den igen från samma Docker registry.

jake@debian10:~$ docker image pull ubuntu:20.04
20.04: Pulling from library/ubuntu
...
docker.io/library/ubuntu:20.04

Nu när jag hämtat hem imagen lägger jag till en ny tagg för imagen för min nya registry.

jake@debian10:~$ docker image tag ubuntu:20.04 registry.labs.cyberinfo.se/ubuntu:20.04

Därefter testar jag att ladda upp den till min nya registry med docker image push.

jake@debian10:~$ docker image push registry.labs.cyberinfo.se/ubuntu:20.04
The push refers to repository [registry.labs.cyberinfo.se/ubuntu]
8891751e0a17: Pushed
2a19bd70fcd4: Pushed
9e53fd489559: Pushed
7789f1a3d4e9: Pushed
...

Nu kan vi se att filerna finns i min S3-bucket som jag skapade tidigare. Ubuntu-katalogen finns under sökvägen /docker/registry/v2/repositories/ i S3-bucketen. Se bilden nedan.

S3-bucket

Därefter raderar jag imagen helt från min lokala dator.

jake@debian10:~$ docker image rm ubuntu:20.04
...

jake@debian10:~$ docker image rm registry.labs.cyberinfo.se/ubuntu:20.04
...

Och till sist testar jag om det går att ladda ner imagen från min egna Docker registry.

jake@debian10:~$ docker image pull registry.labs.cyberinfo.se/ubuntu:20.04
20.04: Pulling from ubuntu
d51af753c3d3: Pull complete
fc878cd0a91c: Pull complete
6154df8ff988: Pull complete
fee5db0ff82f: Pull complete
...
registry.labs.cyberinfo.se/ubuntu:20.04

Allt fungerar således. Dessutom lastbalanserar svärmen trafiken mellan noderna på egen hand. Route53 kommer att rotera så att domännamnet pekar lika ofta på alla noder, men från samma dator kan man få samma IP i ett par minuter som svar. Men internt kommer trafiken distribueras mellan samtliga noder.

Test av high availability

För att testa HA-funktionen stänger jag ner nod1. Efter några minuter ska Route53 bara ge IP-adresserna för nod2 och nod3 som svar. Eftersom Docker Swarm sköter all lastbalansering själv, spelar det ingen roll vilken nod som trafiken kommer in på.

root@nod1~#> poweroff

På min arbetsstation testar jag först att pinga registry.labs.cyberinfo.se med jämna mellanrum för att se så jag bara får nod2 och nod3 som svar.

jake@debian10:~$ ping registry.labs.cyberinfo.se
PING registry.labs.cyberinfo.se (81.4.110.173) 56(84) bytes of data.
64 bytes from 81.4.110.173: icmp_seq=1 ttl=51 time=26.9 ms

jake@debian10:~$ ping registry.labs.cyberinfo.se
PING registry.labs.cyberinfo.se (81.4.109.77) 56(84) bytes of data.
64 bytes from 81.4.109.77: icmp_seq=1 ttl=52 time=24.9 ms

jake@debian10:~$ $ ping registry.labs.cyberinfo.se
PING registry.labs.cyberinfo.se (81.4.110.173) 56(84) bytes of data.
64 bytes from 81.4.110.173: icmp_seq=1 ttl=52 time=24.9 ms

Trafiken går således till bara till nod2 och nod3. Vi kan även kontrollera hur det ser ut i svärmen nu när en nod ligger nere. Följande kommando körs på nod2.

root@nod2~#> docker stack ps registry
ID               NAME                         IMAGE          NODE     DESIRED STATE       CURRENT STATE                
iydz1a6r4yi6     registry_my-registry.1       registry:2     nod2     Running             Running 22 hours ago
gnu9x5eopxoq     registry_my-registry.2       registry:2     nod3     Running             Running about a minute ago
butpnatclq9l      \_ registry_my-registry.2   registry:2     nod1     Shutdown            Running 2 minutes ago
i9unuiuvw0ib     registry_my-registry.3       registry:2     nod3     Running             Running 29 minutes ago

Här ser vi att det fortfarande körs tre stycken replicas. Servicen med namnet registry_my-registry.2 som tidigare kördes på nod1 har nu flyttats över till nod3. Nod3 kör således två kopior nu.

Om vi tittar på noderna nu ser vi att vi nod1 är nere, och nod två har tagit över rollen som ledare.

root@nod2~#> docker nods ls
ID                            HOSTNAME     STATUS      AVAILABILITY     MANAGER STATUS    ENGINE VERSION
w8gg0fzdr6po7gqq3cu58bqtp     nod1         Down        Active           Unreachable       19.03.8
sa4kez0jkcs8g00s2dpvuxjvy *   nod2         Ready       Active           Leader            19.03.8
vogsn1bh3tg95x41hncygnkvf     nod3         Ready       Active           Reachable         19.03.8

Nu är det dags att återigen radera den lokala Ubuntu-imagen och testa att ladda ner den från den egna registryn.

jake@debian10:~$ docker image rm registry.labs.cyberinfo.se/ubuntu:20.04
....

jake@debian10:~$ docker pull registry.labs.cyberinfo.se/ubuntu:20.04
20.04: Pulling from ubuntu
...
Status: Downloaded newer image for registry.labs.cyberinfo.se/ubuntu:20.04
registry.labs.cyberinfo.se/ubuntu:20.04

Registryn fungerar således fortfarande!

Balansera svärmen när nod1 kommer upp igen

När nod1 återansluter sig till svärmen kommer den inte automatiskt att få tillbaka den task (containern för Docker registry) den tidigare körde. Det är gjort så för att inte svärmen konstant ska balansera om sig och därmed orsaka störningar. Med tiden, när nya tasks skapas och försvinner, kommer svärmen att bli balanserad. Men i mitt fall vill jag att alla noder ska köra varsin Docker registry för HA. Jag kan därför manuellt be svärmen att balansera om sig med nedansående kommado.

root@nod1~#> docker service update --force registry_my-registry
registry_my-registry
overall progress: 3 out of 3 tasks
1/3: running   [==================================================>]
2/3: running   [==================================================>]
3/3: running   [==================================================>]
verify: Service converged

Nu kör alla noder varsin Docker registry igen.

Lista dina images i Docker registryn

Det går att lista alla images som finns i registryn med curl. I exemplet nedan ser vi att jag har laddat upp några fler images.

jake@workstation~$> curl -u jackbenny https://registry.labs.cyberinfo.se/v2/_catalog
Enter host password for user 'jackbenny':
{"repositories":["debian","drupal","mycow","nginx","ubuntu"]}

Avslutning

Att göra något liknande med traditionella HA-lösningar (innan Docker och innan molnet) hade varit betydligt mer komplicerat och tidskrävande. Förmodligen hade det tagit dagar, kanske till och med veckor, att sätta upp denna labben. Här tog det mig istället i runda tal cirka två–tre timmar. I den tiden har jag räknat bort tiden det tog mig att göra research, men räknat in tiden för tester med mera. Dessutom kan jag enkelt flytta min labb till andra servrar eller andra molnleverantörer. Jag kan också enkelt utöka svärmen till att omfatta fler noder. Noderna kan befinna sig hos olika molnleverantörer, eller on-prem på egna servrar.