Programozott felhőgyár

Docker Swarm klaszter építése Terraform és Ansible segítségével

Manapság már néhány kattintással és nagyjából egy pizza árának befizetésével lehet egy hónapra saját gépünk a világ bármely pontján, az általunk választott operációs rendszerrel, fix IPv4 címmel és némi adatforgalommal az árban. Persze az a trükk, hogy manapság egy gép már nem gép. Klaszterek kellenek, magas rendelkezésre állás és ha nem figyelünk oda, hirtelen már nyolc-tíz pizzánál járunk.

Teljesen más az is, amikor csak néhány gépet kell kezelnünk. Mindegyiknek gondosan választunk nevet, a legnagyobb szeretettel és odafigyeléssel telepítjük rájuk a szoftvereket, néha talán még eszünkbe is jut belépni rájuk, hogy frissítsük is őket. Aztán elérkezik az a pont, amikor már túl sokan vannak, nem törődünk a nevükkel se, csak a funkciójuk után csapunk egy 01-est, 02-est, 03-ast.

Kísérleti alanyunk egy három gépes Docker Swarm klaszter lesz, amit különböző kódokkal és konfigurációkkal fogunk létrehozni, a virtuális gépektől kezdve, a domain-eken és szoftvereken át egészen a futó alkalmazásokig.

Három gép azért persze még nem a magas rendelkezésre állás csúcsa, de a módszer akár több száz géppel is ugyanilyen jól tud működni. A türelmetlenek megtekinthetik a végeredményt a kapcsolódó Github repóban. Vágjunk is bele.

Infrastruktúra

Először is elő kellene varázsolnunk néhány gépet a semmiből. Ami ebben segíteni fog nekünk, az Infrastructure as Code mozgalom jeles képviselője: Terraform. Az eszköz rengeteg szolgáltatót támogat, mint például az AWS vagy a GCP, de én a jó öreg DigitalOcean-nel mentem a példában.

A Terraform hatásköre esetünkben addig terjed, hogy létrehozzuk vele a gépeket, beletesszük őket egy projektbe és egy saját privát hálózatba, valamint rájuk mutató DNS rekordokat is készítünk (ehhez kell az is, hogy a DigitalOcean kezelje a domain-ünk DNS-ét). Nézzünk is meg egy rövidebb részt a konfigurációból.

terraform/swarm01.tf
provider "digitalocean" {
  version = "~> 1.18"
}

locals {
  region = "fra1"
  image  = "ubuntu-20-04-x64"
  size   = "s-1vcpu-1gb"
}

data "digitalocean_ssh_key" "default" {
  name = "name_of_the_ssh_key"
}

data "digitalocean_domain" "default" {
  name = "example.com"
}

resource "digitalocean_vpc" "swarm01" {
  name   = "swarm01"
  region = local.region
}

resource "digitalocean_droplet" "manager01" {
  image  = local.image
  name   = "manager01.swarm01.example.com"
  region = local.region
  size   = local.size
  private_networking = true
  vpc_uuid = digitalocean_vpc.swarm01.id
  ssh_keys = [
    data.digitalocean_ssh_key.default.fingerprint
  ]
}

resource "digitalocean_record" "manager01" {
  domain = data.digitalocean_domain.default.name
  type   = "A"
  name   = "manager01.swarm01"
  value  = digitalocean_droplet.manager01.ipv4_address
  ttl    = "3600"
}

A data blokkok már létező dolgok, amiket a Terraform nem fog megpróbálni menedzselni, csak elfogadja a létezésüket és a későbbiekben hivatkozhatunk rájuk. A resource blokkok azok, amiket el fog készíteni (és el is tud törölni). Sajnos a droplet nevénél nem tudunk hivatkozni a DNS rekord FQDN-jére, mert a rekordban már hivatkozunk a droplet IPv4 címére, a körkörös hivatkozásokat pedig nem nagyon szereti a Terraform.

A DNS rekordoknál érdemes lehet az elején az itt megadott TTL-nél alacsonyabbal kezdeni, hogy amíg próbálgatjuk a rendszert, törlünk, újra létrehozunk gépeket, ne kelljen túl sokat várni arra, hogy a domain-ek frissüljenek.

Az infrastruktúra létrehozásához még szükségünk lesz egy DIGITALOCEAN_ACCESS_TOKEN környezeti változóra, aztán már csak egy terraform init parancs, hogy letöltődjön az általunk megadott provider és egy terraform apply, némi várakozás és máris három új gép büszke tulajdonosai lehetünk.

Egy kis tipp, hogy hogyan lehet elkerülni azt, hogy ez az eléggé széles körű, mondhatni már-már veszélyes jogosultságokkal rendelkező API token bekerüljön a shell-ünk history fájljába:

$ read -s DIGITALOCEAN_ACCESS_TOKEN
$ export DIGITALOCEAN_ACCESS_TOKEN

Szoftverek

Bárhogy is szeretnénk, maguktól nem lesz ezekből a gépekből Swarm klaszter. Az egyszerű skálázhatóság érdekében azt sem szeretnénk, hogy egyesével kelljen az összes gépen kézzel telepítenünk és beállítanunk mindent.

Szerencsére ezt a problémát is megoldották már mások, használhatjuk például az Ansible-t, ami YAML konfigurációs fájlok alapján tud gépeket felkonfigurálni. Mivel korábban Ubuntu-s gépeket választottunk, így ezek a konfigurációk némileg Debian/Ubuntu specifikusak lesznek, de maga az eszköz sokféle operációs rendszert támogat.

Az Ansible-nek először is tudnia kell, hogy milyen gépekkel dolgozik. Ehhez szüksége van egy hosts fájlra:

ansible/hosts
[managers]
manager01.swarm01.example.com

[workers]
worker[01:02].swarm01.example.com

A konfigurációs fájlok nagy részét a task-ok teszik ki, mint például az apt, amivel nem túl meglepő módon azt tudjuk megadni, hogy milyen csomagokat szeretnénk a gépünkön tudni:

ansible/roles/docker/tasks/main.yml
- name: install prerequisit packages
  apt:
    name:
    - apt-transport-https
    - ca-certificates
    - curl
    - gnupg-agent
    - software-properties-common
    - python3-pip
    update_cache: yes

De létezik modul a Docker Swarm kezelésére is. Például így tudunk inicializálni egy manager node-ot:

ansible/roles/swarm-manager/main.yml
- name: init docker swarm
  docker_swarm:
    advertise_addr: "{{ ansible_facts.eth1.ipv4.address }}"
    state: present

Vagy egy kicsit bonyolultabb, a worker-ek beléptetése a Swarm klaszterbe:

ansible/roles/swarm-worker/main.yml
- name: get swarm info
  docker_swarm_info:
  delegate_to: "{{ groups.managers[0] }}"
  register: swarm_info
- name: join swarm
  docker_swarm:
    state: join
    join_token: "{{ swarm_info.swarm_facts.JoinTokens.Worker }}"
    remote_addrs:
    - "{{ hostvars[groups.managers[0]]['ansible_eth1']['ipv4']['address']}}"
    advertise_addr: "{{ ansible_facts.eth1.ipv4.address }}"

A worker-ek beállításához először el kell kérnünk a join token-t a manager node-tól, hogy csatlakozni tudjanak. A manager node IPv4 címének elérése távolról se nevezhető elegánsnak, de nem sikerült szebb megoldást találnom rá.

Az ansible-playbook setup.yml parancs futtatása és némi várakozás után remélhetőleg van egy működő Swarm klaszterünk. Ezen a ponton érdemes lehet még elgondolkodni a gépek biztonságosabbá tételén, lehet például tűzfal szabályokat beállítani és fail2ban-t telepíteni további YAML fájlok írásával.

Alkalmazások

Mielőtt még bármilyen más alkalmazást telepítenénk a klaszterre, szükségünk lesz egy reverse proxy-ra, ami el tudja dönteni, hogy melyik futó szolgáltatásnak kell kiszolgálnia egy bejövő kérést és továbbítani tudja neki.

A Traefik remekül passzol az eddig felépített rendszerünkhöz. Docker-es címkék alapján fel tudja konfigurálni magát és még Let's Encrypt-es certificate-eket is tud gyártani a biztonságos kapcsolatoknak.

Hogy hogyan kerül ki a stack a Swarm-ra? Csak a megfelelő Compose file formátumú YAML-t kell megírnunk, a többit elintézi az Ansible erre specializálódott modulja.

ansible/roles/stacks/tasks/main.yml
- name: generate traefik stack
  template:
    src: traefik.yml.j2
    dest: /etc/swarm/traefik.yml

- name: create traefik config directory
  file:
    path: /etc/traefik/acme
    state: directory

- name: create traefik network
  docker_network:
    name: traefik-net
    driver: overlay

- name: deploy traefik stack
  docker_stack:
    name: traefik
    compose:
    - /etc/swarm/traefik.yml

Felmásoljuk a gépre a Traefik-hez tartozó Compose fájlt, létrehozzuk a Let's Encrypt certificate-ek tárolására használt könyvtárat, létrehozunk még egy Docker-es hálózatot a Traefik és az őt használó alkalmazások számára és végül deploy-oljuk a stack-et.

A Compose fájl nem a legegyszerűbb jószág, rengeteg mindent kell beállítanunk, hogy működjenek a dolgok és a címkés módja a konfigurációnak nem éppen a legbarátságosabb.

ansible/roles/stacks/templates/traefik.yml.j2
version: "3.2"
services:
  traefik:
    image: traefik:v2.2.1
    command:
      - "--accesslog=true"

      # Engedélyezzük az API-t és a Dashboard-ot
      - "--api=true"
      - "--api.dashboard=true"

      # Docker Swarm-ból fognak jönni a routing konfigurációk
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.swarmMode=true"

      # 80-as és 443-s porton is kiszolgálunk tartalmakat
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"

      # Szeretnénk automatikus TLS certificate-eket
      - "--certificatesresolvers.myresolver.acme.email=something@example.com"
      - "--certificatesresolvers.myresolver.acme.storage=/etc/traefik/acme/acme.json"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"
      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
    deploy:
      # Ez és a portoknál a host mód azért kell, hogy a Traefik a megfelelő
      # request address-t írja az access log-ba
      mode: global
      placement:
        constraints:
          - node.role == manager
      labels:
        - "traefik.enable=true"
        - "traefik.docker.network=traefik-net"

        # Basic auth middleware a Dashboard-nak
        - "traefik.http.middlewares.test-auth.basicauth.users=test:$$2y$$05$$ooiVn4yz0coSR28J9O5wNuvGHyZPCAaRYSeDXDdKCkbbtKO31LJ1K"
        - "traefik.http.middlewares.test-auth.basicauth.removeheader=true"

        # Redirect middleware, hogy automatikusan irányítson át http-ről https-re
        - "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
        - "traefik.http.middlewares.test-redirectscheme.redirectscheme.permanent=true"

        # A http endpoint-on csak redirect-elünk
        - "traefik.http.routers.api.rule=Host(`traefik.swarm01.example.com`)"
        - "traefik.http.routers.api.middlewares=test-redirectscheme"
        - "traefik.http.routers.api.entrypoints=web"

        # A https endpoint-on kiszolgáljuk az API-t (és a Dashboard-ot)
        - "traefik.http.routers.api-secure.rule=Host(`traefik.swarm01.example.com`)"
        - "traefik.http.routers.api-secure.service=api@internal"
        - "traefik.http.routers.api-secure.middlewares=test-auth"
        - "traefik.http.routers.api-secure.entrypoints=websecure"
        - "traefik.http.routers.api-secure.tls=true"
        - "traefik.http.routers.api-secure.tls.certresolver=myresolver"

        - "traefik.http.services.dummy-svc.loadbalancer.server.port=9999"
    volumes:
      # A certificate-eket a host gépre mentjük
      - /etc/traefik/acme:/etc/traefik/acme
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - traefik-net

networks:
  traefik-net:
    external:
      name: traefik-net

A szemfülesebbek észrevehették, hogy template-et használtunk sima fájl helyett, aminek jelen esetben nincs jelentősége, de később még rátérünk, hogy miért lehet hasznos a Compose fájlokat template-ként kezelni.

Az Ansible playbook futtatása után (ansible-playbook deploy.yml), ha minden jól ment van egy működő Traefik Dashboard-unk a https://traefik.swarm01.example.com/ címen.

Saját alkalmazások telepítése hasonló módon történhet. Valamilyen úton-módon lebuild-eljük belőlük a Docker image-et, amit felküldünk egy registry-be, írunk hozzá egy Compose fájlt, amit megfelelően felcímkézünk a Traefik számára és végül Ansible segítségével deploy-oljuk.

Nézzük is meg a példa kedvéért, hogy egy whoami alkalmazást hogyan konfigurálhatnánk fel. Maga az alkalmazás nem sok mindent csinál, a kapott kérésről jelenít meg információkat (ki szolgálta ki, milyen header-öket kapott). Swarm tesztelős próba alkalmazásnak tökéletesen megteszi, például az is látszik a kimenetéből, hogy az egyes kéréseket más-más worker-ek szolgálják ki.

ansible/roles/stacks/templates/whoami.yml.j2
version: "3.2"
services:
  whoami:
    image: containous/whoami:v1.5.0
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      labels:
        - "traefik.enable=true"
        - "traefik.docker.network=traefik-net"

        # Redirect middleware, hogy automatikusan irányítson át http-ről https-re
        - "traefik.http.middlewares.test-redirectscheme.redirectscheme.scheme=https"
        - "traefik.http.middlewares.test-redirectscheme.redirectscheme.permanent=true"

        # A http endpoint-on csak redirect-elünk
        - "traefik.http.routers.whoami.rule=Host(`whoami.swarm01.example.com`)"
        - "traefik.http.routers.whoami.middlewares=test-redirectscheme"
        - "traefik.http.routers.whoami.entrypoints=web"

        # A https endpoint-on kiszolgáljuk az alkalmazást
        - "traefik.http.routers.whoami-secure.rule=Host(`whoami.swarm01.example.com`)"
        - "traefik.http.routers.whoami-secure.entrypoints=websecure"
        - "traefik.http.routers.whoami-secure.service=whoami_service"
        - "traefik.http.routers.whoami-secure.tls=true"
        - "traefik.http.routers.whoami-secure.tls.certresolver=myresolver"

        - "traefik.http.services.whoami_service.loadbalancer.server.port=80"
    networks:
      - traefik-net
    environment:
      - "SECRET={{ secret }}"

networks:
  traefik-net:
    external:
      name: traefik-net

A címkézős rész nagyon hasonlít a Traefik címkézős részéhez. Az environment résznél van egy kis érdekesség, a korábban említett template fájlos móka. A szenzitív adatokat tárolhatjuk encrypt-ált Ansible Vault-ban:

ansible/roles/stacks/vars/main.yml
secret: !vault |
        $ANSIBLE_VAULT;1.1;AES256
        30326530326164646161636239316436333339393835633164396535373739323933333432333937
        3536623538346139333639626263386263353333656565660a663763616335356561613366323964
        64633962643139656362356164373633616165333130623034383561383165396137623136653734
        6334316262626336660a343739343036323533626330633365643439623233663234396264376639
        3333

A playbook futtatása során az Ansible bekéri a jelszót (ha az --ask-vault-pass kapcsolóval van futtatva) és a template-be már a megfelelően decrypt-ált értéket helyettesíti be.

Monitorozás

Beléphetünk a manager gépre és kiadhatunk docker parancsokat, de akár a docker parancsot is rábírhatjuk arra, hogy ssh-zzon be helyettünk:

$ docker context create swarm01 --docker "host=ssh://root@manager01.swarm01.example.com"
$ docker context use swarm01
$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                      PORTS
hnjenj5royp1        traefik_traefik     global              1/1                 traefik:v2.2.1
b8gwkvtih4z6        whoami_whoami       replicated          3/3                 containous/whoami:v1.5.0
$ docker service logs -f traefik_traefik
[...]

A kényelmesebb monitorozás érdekében házi feladatként felhúzhatunk még egy Elastic Stack-et is (szigorúan Terraform és Ansible segítségével) és becsatornázhatjuk oda gépeink logjait.

Skálázás

A legegyszerűbb eset ha csak a worker-ek bizonyulnak kevésnek. Csak még egy droplet blokk a Terraform konfigban, egy kis módosítás az Ansible hosts fájlban és máris van egy extra worker-ünk. Ezen kívül természetesen az egyes worker droplet-ek teljesítményét is növelhetjük (több processzor mag, több memória).

Ha a manager node kezd kevés lenni, az már kicsit bonyolultabb. A droplet teljesítmény egyszerűen növelhető itt is, de ha már nincs hova skálázni őket, az bizony problémás lehet. A több manager node inkább a rendelkezésre állás javítására lett kitalálva, nem teljesítmény növelésre és egyébként sok más problémát is magával hoz (például a certificate-eket már nem tárolhatnánk fájlban). Viszont egyszerűen felhúzhatunk egy teljes swarm02 klasztert is és az alkalmazások egy részét átmigrálhatjuk oda.

Egy másik skálázási probléma az lehet, ha egy személyes hadsereg helyett egy vagy akár több csapatnyi embernek kell ezt a rendszert használni. Itt már nem opció az, hogy mindenki veszélyes API tokenekkel szaladgál és a gépéről futtatja a terraform apply-t. Érdemes lehet mondjuk egy build szerverre bízni ezt, a módosítás folyamatát pedig merge request-ekkel megoldani. Ennek segítésére a Terraform ad egy terraform plan parancsot, ami megpróbálja felvázolni, hogy mit fog csinálni a terraform apply, valamint az Ansible-t is futtathatjuk a --check kapcsolóval, ami csak kiírja, hogy milyen módosításokat kellene végrehajtani.

Összegzés

A Terraform és az Ansible segítségével több száz gépet (és egyéb szolgáltatást) húzhatunk fel gyorsan és egyszerűen a különböző felhő szolgáltatóknál. A Docker Swarm és a Traefik pedig gondoskodik az alkalmazásainkról és rengeteg egyéb terhet levesz a vállunkról (pl. skálázás, certificate-ek). Ráadásul mindezt úgy, hogy az egész plain text, verziókövethető szövegfájlokban van leírva.

This post is also available in english: Scripted cloud factory

Hozzáfűznél valamit?

Dobj egy emailt a blog kukac deadlime pont hu címre.

Feliratkoznál?

Az RSS feed-et ajánljuk, ha kedveled a régi jó dolgokat.