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ů.
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ší.
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.