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.

