Rychlé testy webu v plné palbě díky Gitlab CI a Kubernetes Ingressu

4. 8. 2021
Doba čtení: 6 minut

Sdílet

 Autor: Seznam.cz
Automatizace a kontejnery dnes umožňují snadno realizovat věci, kvůli kterým jsme dřív museli udržovat křehké sady skriptů. Ukážeme si, jak zařídit, aby se pro každou pushnutou větev automaticky nasadila vlastní instance webu.

Uspořádání běhových prostředí se liší firmu od firmy a někdy i tým od týmu. Jak přesně vypadají, záleží na povaze projektů, které děláte, a na datech, která držíte. V každém případě potřebujete provoz, aby měli kam chodit uživatelé. Pokud nejsou extrémně tolerantní vůči chybám, většinou chcete i prostředí vývojové, testovací a případně staging.

Zejména pro weby a větší týmy, které stíhají dělat více věcí najednou, se hodí možnost ve vývojovém prostředí nasadit a rozjet libovolnou větev. Můžete tak ukázat stav a vzhled nových funkcí, ještě než je pustíte do dalších fází, kde už jsou případné změny komplikovanější. Není to nic, k čemu byste potřebovali trendové věci, jako jsou kontejnery nebo Ingress objekty, ale s jejich pomocí jde vše snadněji. Zejména pokud chcete cokoli složitějšího než vypuštění webserveru na hromadu souborů.

Servery Seznam.cz v datacentru Kokura
Autor: Seznam.cz

Servery Seznam.cz v datacentru Kokura

V tomto článku popíšeme uspořádání a jednotlivé nástroje, ze kterých jsme si postavili CI pipelines v Gitlabu tak, aby kromě „běžných“ nasazení do všech prostředí pro každou větev automaticky vytvořily i endpoint na https://jmeno-vetve.zbozi.interni.domena , na kterém je v plné parádě vidět všechno, co dotyčná větev páchá. Používáme k tomu:

Šablonování konfigurací

Máme-li dva servery, je možné žít v iluzi, že nastavení nginxu je konfigurační soubor, který si nastavíme jednou pro test, jednou pro provoz, a je hotovo. U nás tato iluze začala vykazovat povážlivé trhliny, když jsme do dvou souborů přidávali nové sekce pro další API cesty a měnili jsme parametry logování. Při pokusu udržovat web i pro vývojové prostředí se pak rozpadla zcela.

Od té doby generujeme konfigurace nginxu přes goenvtemplator – nástroj využívající proměnné prostředí a šablonovací modul zabudovaný v Go. Nemá sice všechny funkce složitějších šablonovacích jazyků, jako je Jinja nebo PHP, ale vynahrazuje to snadnou instalací; prostě nakopírujete jeden binární soubor.

Výsledek potom vypadá tak, že v konfiguraci máte něco takového:

http {
    upstream api {
        server {{ env "PROXY_API" }};
        keepalive 16;
    }

    server {
        listen *:{{ env "PROXY_HTTP_PORT" }};
        server_name {{ env "PROXY_HOST" }};

        location /api {
            proxy_pass http://api;
        }


        location ~ ^/(img/|robots\.txt$) {
            expires 24h;
        }
    }
}

Docker i Kubernetes mají pro propagaci proměnných prostředí mechanismy. Před spuštěním serveru pak jen ze sady proměnných, které se ve vašem prostředí skutečně mění, vytvoříme skutečnou běhovou konfiguraci, což v kontejneru zařídí entrypoint skript:

#!/bin/bash
set -e
goenvtemplator2 -template "/app/nginx.conf:/app/nginx.conf.runtime"
exec nginx -c /app/nginx.conf.runtime

Kubernetes manifesty

Když CI pipeline vytvoří Docker image, můžeme úplně stejně šablonovat i manifesty pro Kubernetes. V .gitlab-ci.yml (případně jeho ekvivalentu pro vaše oblíbené CI) pak bude přibližně následující sekce:

k8s-dev-branch:
  script:
  - docker build -t my-repo/my-web-build:p${CI_PIPELINE_ID} .
  - docker push my-repo/my-web-build:p${CI_PIPELINE_ID}
  - goenvtemplator2
      -template $(pwd)/k8s/deploy.yaml.templ:$(pwd)/k8s/deploy.yaml
  - kubectl apply --record -f k8s/
  - kubectl rollout status -f k8s/deploy.yaml --timeout=3m
  only:
  - branches

V Kubernetes clusteru a okolo něj pak budeme mít:

  • Deployment s image z naší větve
  • ClusterIP Service nasměrovanou na Deployment
  • Ingress konfiguraci, která pro dynamicky generovaný hostname bude terminovat TLS a směrovat provoz na dotyčnou službu
  • DNS záznam *.web.interni.domena směřující na Kubernetes ingress

Hvězdičkový záznam v DNS a odpovídající TLS certifikát nám stačí udělat jednou, zbytek objektů vytvoříme ze šablony v průběhu buildu. Formát YAML umožňuje jednotlivé objekty buď spojit do jednoho souboru a oddělit pomocí řádku s ---, nebo je rozdělit do jednotlivých souborů. Pak je ovšem musíme všechny prohnat substitucí proměnných.

Deployment využívá více labelů, které se nám budou hodit k rozlišení objektů patřících k jednotlivým větvím. Ve svém třetím umístění (v šabloně podu ve .spec.template.metadata) nejsou striktně vzato nutné, ale dovolují přes labely najít i jednotlivé pody. V GitLabu je praktická proměnná CI_COMMIT_REF_SLUG, kde je jméno větve, již normalizované tak, že jej můžeme rovnou použít v doméně.

Do kontejneru s webem předáváme spíše jen jako příklad adresu na interní endpoint s API. V praxi je na Zboží.cz takových API několik, takže ve zdrojácích máme soubory s adresami backendů v jednotlivých prostředích, které se pak vyberou a přidají goenvtemplatoru v parametru -env-file, a tudy se dostanou do konfigurace deploymentu.

Šablona deploymentu potom vypadá takto:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-dev-{{ env "CI_COMMIT_REF_SLUG" }}
  labels:
    app: web
    kind: dev-branch
    branch: {{ env "CI_COMMIT_REF_SLUG" }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
      kind: dev-branch
      branch: {{ env "CI_COMMIT_REF_SLUG" }}
  template:
    metadata:
      name: web-dev-{{ env "CI_COMMIT_REF_SLUG" }}
      labels:
        app: web
        kind: dev-branch
        branch: {{ env "CI_COMMIT_REF_SLUG" }}
    spec:
      containers:
      - name: web
        image: my-repo/my-web-build:p{{ env "CI_PIPELINE_ID" }}
        env:
        - name: PROXY_HOST
          value: {{ env "CI_COMMIT_REF_SLUG" }}.web.interni.domena
        - name: PROXY_HTTP_PORT
          value: "8000"
        - name: PROXY_API
          value: nejake-api.interni.domena
        ports:
        - name: http
          containerPort: 8000

Dále potřebujeme Kubernetes service, která zpřístupní náš webový port v clusteru, to jsou víceméně jen vypsané labely a porty:

kind: Service
apiVersion: v1
metadata:
  name: web-dev-{{ env "CI_COMMIT_REF_SLUG" }}
  labels:
    app: web
    kind: dev-branch
    branch: {{ env "CI_COMMIT_REF_SLUG" }}
spec:
  type: ClusterIP
  selector:
    app: web
    kind: dev-branch
    branch: {{ env "CI_COMMIT_REF_SLUG" }}
  ports:
  - name: https
    protocol: TCP
    port: 8000
    targetPort: 8000

A konečně Ingress. Ten bude odkazovat na vytvořenou service a interně spojovat všechny naše deploymenty pod jednu IP adresu. Tudíž bude muset terminovat TLS. K tomu účelu mu budeme muset vygenerovat a dodat wildcard certifikát a dodat mu jej v Kubernetes Secretu. Jak je zvykem v dobrých kuchařkách, i zde platí: kdo nemá certifikát, sekci tls vynechá a web mu poběží na nezabezpečeném HTTP.

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  annotations:
    ingress.kubernetes.io/ssl-passthrough: "false"
    nginx.ingress.kubernetes.io/ssl-passthrough: "false"
    nginx.ingress.kubernetes.io/secure-backends: "false"
  name: web-dev-{{ env "CI_COMMIT_REF_SLUG" }}
  labels:
    app: web
    kind: dev-branch
    branch: {{ env "CI_COMMIT_REF_SLUG" }}
spec:
  rules:
  - host: {{ env "CI_COMMIT_REF_SLUG" }}.web.interni.domena
    http:
      paths:
    - backend:
          serviceName: web-dev-{{ env "CI_COMMIT_REF_SLUG" }}
          servicePort: 8000
        path: /
  tls:
  - hosts:
    - {{ env "CI_COMMIT_REF_SLUG" }}.web.interni.domena
    secretName: web-dev-tls

Kam dál?

Popsané kousky nám dohromady daly GitLab CI pipeline, která po pushi jakékoli větve do pár minut vystaví webovou stránku s odpovídající podobou webu. Hodí se pro rychlé testování i pro předvedení složitějších funkcí, a oproti sadě skriptů, která tvořila checkouty větví na portech jednoho nešťastného virtuálního serveru, je celkově přehlednější a udržitelnější.

bitcoin_skoleni


Autor: Petr Novák

Vzájemné pospojování objektů v Kubernetes clusteru

Po nějaké době čilého vývoje jsme ale narazili na problém: v clusteru se nám začaly hromadit deploymenty. To jsme nakonec překonali dalším skriptem, který se spustí každý večer a porovná existující větve v GitLabu se seznamem objektů v Kubernetu. Ty mají v labelech označeno normalizované jméno větve, ze které pocházejí.

Potom už skriptu stačilo smazat deploymenty, service a ingressy, pro které neexistuje větev, a v clusteru je opět pořádek. Po důkladném zvážení našeho smyslu pro pořádek jsme ještě skript upravili tak, aby mazal i větve, které nikdo neaktualizoval déle než dva týdny, a od té doby žijeme šťastně až do smrti.

Autor článku

Vystudoval informatiku na Matfyzu a pracuje v Seznamu jako vedoucí vývoje.