11 - CI/CD¶
K3s Homelab — Sesja 11¶
Data: 2026-03-11
Środowisko: 3x HP T630, k3s v1.34.4, Flux v2.8.1, Cilium v1.19.1
Co zbudowaliśmy¶
- GitHub Actions CI/CD pipeline — automatyczny build i push do DockerHub z semver tagowaniem
- clients-api deployment — Spring Boot + CloudNativePG na klastrze przez GitOps
- ServiceMonitor — integracja z Prometheus, metryki aplikacji widoczne w Grafanie
Czego się nauczyłem¶
1. GitHub Actions — semver tagging¶
Problem z tagowaniem SHA:
Tagi w formacie sha-<short-commit> nie są sortowalny chronologicznie. Flux używa porównania alfabetycznego — sha-e984bdc > sha-9cb3301 literowo, ale e984bdc był starszym commitem. Flux wybierałby losowo "najnowszy" obraz.
Prawidłowe podejście — semver:
# .github/workflows/ci.yml
tags: |
type=semver,pattern={{version}} # v1.1.0 → 1.1.0
type=semver,pattern={{major}}.{{minor}} # v1.1.0 → 1.1
type=sha,prefix=sha-,format=short # każdy push → sha-XXXXXXX
type=raw,value=latest,enable={{is_default_branch}}
Krytyczna pułapka — warunek if na job:
# ŹLE — job pomijany gdy github.ref = refs/tags/v1.1.0
if: github.ref == 'refs/heads/main'
# DOBRZE — buduj też na tagach
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
Gdy pushujesz git tag v1.1.0, github.ref ma wartość refs/tags/v1.1.0 — nie refs/heads/main. Job z samym warunkiem main jest w całości pomijany, semver tagi nigdy nie powstają.
Workflow po pushu taga:
git tag v1.1.0 && git push origin v1.1.0
↓
GitHub Actions: test → build-and-push (if: main OR tags/v*)
↓
DockerHub:
kcn333/clients-api:1.1.0 ← semver
kcn333/clients-api:1.1 ← semver major.minor
kcn333/clients-api:sha-f7e2680 ← SHA tego commita
kcn333/clients-api:latest ← raw
Konwencja semver:
v1.0.0 ← major: breaking changes
v1.1.0 ← minor: nowa funkcjonalność, backwards compatible
v1.1.1 ← patch: bugfix
2. Flux ImagePolicy — semver vs alphabetical¶
Kiedy używać semver:
Kiedy NIE używać alphabetical dla SHA:
SHA nie ma gwarancji porządku chronologicznego — sha-e984bdc może być starszy od sha-9cb3301 mimo że alphabetycznie jest "większy". Semver jest deterministyczny i jednoznaczny.
Weryfikacja że ImagePolicy widzi nowy tag:
flux get images policy clients-api -n flux-system
# NAME IMAGE TAG READY MESSAGE
# clients-api kcn333/clients-api 1.1.0 True Latest image tag resolved to 1.1.0
3. Deploy aplikacji Spring Boot na k3s¶
Minimalne zasoby do działającego deploymentu:
Kluczowe env vars dla Spring Boot:
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod" # ← aktywuje application-prod.properties
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: clients-db-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: clients-db-secret
key: password
Diagnoza profilu aplikacji przez logi:
# Zły profil (domyślny):
"url=jdbc:h2:mem:..." ← H2 in-memory
"Exposing 1 endpoint beneath base path '/actuator'"
# Dobry profil (prod):
"The following 1 profile is active: \"prod\""
"HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection..."
"Database version: 17.4"
"Exposing 4 endpoints beneath base path '/actuator'"
4. ServiceMonitor — integracja z Prometheus¶
Trzy warunki żeby ServiceMonitor działał:
- Label na Service metadata (nie tylko spec.selector!) — Prometheus relabeling sprawdza
__meta_kubernetes_service_label_<label>:
# ŹLE — brak labela w metadata
metadata:
name: clients-api
# DOBRZE
metadata:
name: clients-api
labels:
app: clients-api # ← wymagane dla relabelingu
- Nazwany port w Service — ServiceMonitor odwołuje się do portu przez nazwę:
# ŹLE — brak nazwy portu
ports:
- port: 80
targetPort: 8080
# DOBRZE
ports:
- name: http # ← wymagane
port: 80
targetPort: 8080
- Label
release: kube-prometheus-stackna ServiceMonitor — Prometheus selector szuka tej etykiety:
Diagnostyka ServiceMonitor krok po kroku:
# Krok 1 — Sprawdź czy Prometheus config zawiera job
kubectl exec -n monitoring prometheus-kube-prometheus-stack-prometheus-0 \
-- cat /etc/prometheus/config_out/prometheus.env.yaml | grep -A10 "clients"
# Krok 2 — Sprawdź stan targetów (active vs dropped)
curl -s "http://localhost:9090/api/v1/targets?state=any" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for t in data['data']['activeTargets'] + data['data']['droppedTargets']:
addr = t.get('discoveredLabels', {}).get('__address__', '')
if '8080' in addr:
print('ADDRESS:', addr)
print('HEALTH:', t.get('health', 'DROPPED'))
print('LAST ERROR:', t.get('lastError', ''))
"
# Krok 3 — Jeśli DROPPED bez błędu → problem z relabelingiem (label na Service)
# Jeśli DROPPED z błędem connection refused → UFW lub zły port
# Jeśli active ale health=unknown → za krótki czas od dodania (czekaj 30s)
Typowy błąd — DROPPED bez error message:
Oznacza że target przeszedł discovery (Prometheus go znalazł) ale został odfiltrowany przez reguły relabelingu. Najczęstsza przyczyna: brak labela app: clients-api w metadata.labels na Service (nie mylić z spec.selector).
spec.selector.app = clients-api ← mówi Service gdzie wysyłać ruch (do podów)
metadata.labels.app = clients-api ← mówi Prometheusowi że to właściwy Service
To są dwie niezależne konfiguracje — obie muszą być ustawione.
5. Prometheus serviceMonitorNamespaceSelector¶
Sprawdź przed debugowaniem czy Prometheus w ogóle skanuje właściwy namespace:
{}— skanuje wszystkie namespace ✅matchLabels: ...— tylko namespace z tym labelem
Przy {} i poprawnych labelach na ServiceMonitor — problem zawsze leży w samym Service (brak label w metadata lub brak nazwy portu).
6. Metryki Spring Boot Actuator w Prometheusie¶
Przydatne PromQL queries dla Spring Boot:
# Request rate per endpoint
rate(http_server_requests_seconds_count{application="clients-api"}[5m])
# Error rate (5xx)
rate(http_server_requests_seconds_count{application="clients-api",status=~"5.."}[5m])
# p99 latency
histogram_quantile(0.99, rate(http_server_requests_seconds_bucket{application="clients-api"}[5m]))
# JVM heap usage
jvm_memory_used_bytes{application="clients-api",area="heap"}
# Aktywne połączenia z bazą
hikaricp_connections_active{application="clients-api"}
# Connection pool usage
hikaricp_connections_pending{application="clients-api"}
Tag application="clients-api" pochodzi z konfiguracji w application-prod.properties:
Pełna pętla CI/CD → GitOps¶
Developer: git tag v1.2.0 && git push origin v1.2.0
↓
GitHub Actions: mvn test → docker build → docker push
↓
DockerHub: kcn333/clients-api:1.2.0, :1.2, :sha-XXXX, :latest
↓
Flux ImageRepository: skanuje co 1 minutę
↓
Flux ImagePolicy: semver >=1.0.0 → wybiera 1.2.0
↓
Flux ImageUpdateAutomation: commituje do repo
image: kcn333/clients-api:1.2.0 # {"$imagepolicy": "flux-system:clients-api"}
↓
Flux kustomize-controller: rolling update
↓
Prometheus ServiceMonitor: scrape /actuator/prometheus co 30s
↓
Grafana: metryki aplikacji
Finalna struktura plików clients-api¶
apps/base/clients-api/
├── namespace.yaml
├── db-cluster.yaml ← CloudNativePG cluster
├── db-secret-sealed.yaml ← SealedSecret z credentials BD
├── deployment.yaml ← Spring Boot, prod profile, 2 repliki
├── service.yaml ← port http nazwany (wymagane przez ServiceMonitor)
├── ingress.yaml ← clients-api.cluster.kcn333.com z TLS
├── imagerepository.yaml ← skanuje kcn333/clients-api co 1m
├── imagepolicy.yaml ← semver >=1.0.0
├── servicemonitor.yaml ← scrape /actuator/prometheus co 30s
└── kustomization.yaml
Backlog (do zrobienia)¶
- Grafana dashboard dla clients-api — HTTP metrics, JVM, HikariCP ← następna sesja
- PrometheusRule dla clients-api — alerty na error rate, latency
- NetworkPolicy — izolacja między podami (Cilium gotowy)
- HPA — Horizontal Pod Autoscaler dla clients-api
- Pod Disruption Budget
- Progressive delivery (staging/production branches)
- RBAC — własni użytkownicy
- Hubble UI — Cilium network observability
- HashiCorp Vault
- External-dns
Przydatne komendy¶
# CI/CD — git tagging
git tag v1.x.0
git push origin v1.x.0
git tag -d v1.x.0 && git push origin :refs/tags/v1.x.0 # usuń i puść ponownie
# Flux image automation
flux get images all -n flux-system
flux get images policy clients-api -n flux-system
# Spring Boot diagnostyka
kubectl logs -n clients deploy/clients-api | grep -E "profile|HikariPool|Exposing"
kubectl logs -n clients deploy/clients-api --follow
# ServiceMonitor diagnostyka
kubectl get servicemonitor -n clients
kubectl describe servicemonitor clients-api -n clients
# Prometheus targets
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090 &
curl -s "http://localhost:9090/api/v1/targets?state=any" | python3 -m json.tool | grep -E "clients|health|error"
# Sprawdź config Prometheusa
kubectl exec -n monitoring prometheus-kube-prometheus-stack-prometheus-0 \
-- cat /etc/prometheus/config_out/prometheus.env.yaml | grep -A15 "clients"
# Sprawdź RBAC Prometheusa
kubectl auth can-i get pods \
--as=system:serviceaccount:monitoring:kube-prometheus-stack-prometheus \
-n clients