Laravel na k3s, część 2: CI/CD do Kubernetes

7 min. czytaniaZaktualizowano

Laravel na k3s — Część 1 · Część 2 · Część 3 · Część 4

Pierwsze wdrożenie udowodniło, że Deployment potrafi przyjąć ruch. Ta część sprawia, że zmiana jest powtarzalna: pipeline testuje źródła, buduje jeden niemutowalny obraz, uruchamia osobno widoczną migrację, zmienia workload i weryfikuje ścieżkę publiczną. Celowo nie przekazuje CI kubeconfigu administratora ani nie robi z latest identyfikatora release'u.

Kontrakt bezpieczeństwa przenosi się między GitLab CI, GitHub Actions i innym runnerem. Poświadczenia produkcyjne są chronione; tożsamość budująca i klastrowa są osobne; wdrożony artefakt jest zapisany; obserwowana jest gotowość; a operator ma udokumentowaną ścieżkę odzyskania. Składnia runnera jest drugorzędna. Jeśli release nie odpowiada, który digest obrazu obsługuje ruch, nie jest jeszcze release'em śledzalnym.

Zbuduj raz i promuj ten sam artefakt

Użyj taga SHA commita dla czytelności i zapisz pushnięty digest jako wiążącą tożsamość. Nie buduj ponownie tego samego commita dla produkcji: obraz bazowy, mirror paczek lub czas budowania mogą zmienić bajty. Staging powinien testować dokładnie obraz, który dostanie produkcja. Ten przykład GitLab zapisuje tag w artefakcie dotenv, więc kolejne joby nie wybiorą przypadkiem innej wartości.

yaml
stages: [test, build, migrate, deploy, verify]

variables:
  IMAGE_TAG: "$CI_COMMIT_SHA"

test:
  stage: test
  script:
    - docker compose run --rm app php artisan test --compact

build:
  stage: build
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE:$IMAGE_TAG" .
    - docker push "$CI_REGISTRY_IMAGE:$IMAGE_TAG"
    - printf 'DEPLOY_IMAGE=%s\n' "$CI_REGISTRY_IMAGE:$IMAGE_TAG" > image.env
  artifacts:
    reports:
      dotenv: image.env

deploy-production:
  stage: deploy
  environment: production
  when: manual
  needs: [build]
  script:
    - test -n "$DEPLOY_IMAGE"
    - kubectl -n storefront set image deployment/web web="$DEPLOY_IMAGE"
    - kubectl -n storefront annotate deployment/web "release.example.com/sha=$CI_COMMIT_SHA" --overwrite
    - kubectl -n storefront rollout status deployment/web --timeout=180s

Ręczna bramka nie zastępuje review; jest wąską akceptacją produkcji po przejściu kontroli chronionej gałęzi. W przepływie promocji job staging i produkcyjny muszą konsumować ten sam artefakt DEPLOY_IMAGE. Jeszcze lepiej: po pushu rozwiąż i zapisz digest rejestru, gdy rejestr i runner to wspierają. Tag SHA jest niemutowalny tylko wtedy, gdy polityka rejestru blokuje jego nadpisanie.

Daj CI tożsamość, a nie poświadczenia administratora

Poświadczenie rejestru potrzebuje tylko prawa pushowania obrazów aplikacji. Tożsamość klastra potrzebuje prawa wyłącznie w storefront: stworzyć i obserwować Job migracji, spatchować Deployment web i obejrzeć jego Pody oraz Events. Token mogący zmieniać każdy namespace zamienia przejęty runner w incydent całego klastra.

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-deployer
  namespace: storefront
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ci-deployer
  namespace: storefront
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "patch", "watch"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["get", "list", "create", "delete", "watch"]
  - apiGroups: [""]
    resources: ["pods", "pods/log", "events"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployer
  namespace: storefront
subjects:
  - kind: ServiceAccount
    name: ci-deployer
    namespace: storefront
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ci-deployer

Sposób uwierzytelnienia runnera jest decyzją infrastrukturalną: krótkotrwały token OIDC jest lepszy niż skopiowany, długowieczny kubeconfig. Niezależnie od mechanizmu umieść go w zmiennych chronionego środowiska, rotuj i przetestuj kubectl auth can-i przed dniem release'u. Nie commituj tokenu bearer, nawet jeśli wygląda jak zaszyfrowane pole base64.

sh
kubectl -n storefront auth can-i patch deployment/web
kubectl -n storefront auth can-i create jobs.batch
kubectl -n storefront auth can-i get secrets

Oczekiwaną odpowiedzią na ostatnie polecenie jest no. CI może wskazać Secret runtime istniejący już w namespace, ale nie musi odczytywać jego wartości, by utworzyć kontener aplikacji.

Traktuj migracje jak krok release'u z nazwą

Uruchamiaj migrację raz, przed zmianą podów web, używając tego samego niemutowalnego obrazu. Nazwa Job zawiera krótki SHA commita, dlatego zakończony Job nie zostanie po cichu użyty ponownie. Laravelowe migrate --force potwierdza nieinteraktywny run produkcyjny; nie czyni zmian schematu ani danych odwracalnymi. Pisz migracje według expand-contract: najpierw dodaj nullable column albo nową tabelę, wdróż kod działający z oboma kształtami, wykonaj backfill asynchronicznie, a dopiero później usuń starą ścieżkę.

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: migrate-3f18d7c
  namespace: storefront
  labels:
    app: storefront
    release: 3f18d7c
spec:
  backoffLimit: 1
  ttlSecondsAfterFinished: 86400
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.com/acme/store:3f18d7c
          imagePullPolicy: IfNotPresent
          command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
          envFrom:
            - secretRef:
                name: storefront-runtime
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits: { cpu: 500m, memory: 512Mi }

Wyrenderuj obraz i nazwę z CI zamiast aplikować dosłowny przykład. Utwórz na przykład tymczasowy manifest z reviewowanego szablonu, potem czekaj na niego i pokaż logi również wtedy, gdy zawiedzie:

sh
job="migrate-${CI_COMMIT_SHORT_SHA}"
kubectl -n storefront delete job "$job" --ignore-not-found
sed -e "s|IMAGE_PLACEHOLDER|$DEPLOY_IMAGE|g" \
    -e "s|JOB_PLACEHOLDER|$job|g" k8s/migrate-job.yaml | kubectl apply -f -
if ! kubectl -n storefront wait --for=condition=complete "job/$job" --timeout=180s; then
  kubectl -n storefront logs "job/$job" --all-containers=true || true
  kubectl -n storefront describe "job/$job"
  exit 1
fi
kubectl -n storefront logs "job/$job" --all-containers=true

Nie uruchamiaj php artisan migrate przy starcie aplikacji. Podczas scale-outu dwie repliki mogą wejść w wyścig; przy rollbacku stary kod może wystartować na zmienionym schemacie. Nie ponawiaj też ślepo nieudanej migracji: obejrzyj logi, ustal, czy zapisała częściowe dane, i przygotuj wyraźny release korygujący albo decyzję o odtworzeniu.

Zweryfikuj deploy i wybierz właściwy rollback

Po udanym Job zaktualizuj obraz web, czekaj na rollout i sprawdź publiczny hostname. Zweryfikuj stan control plane oraz ruch widoczny dla użytkownika. Sama zielona readiness probe nie dowodzi DNS, TLS, routingu ingressu ani oczekiwanego obrazu release'u.

sh
kubectl -n storefront set image deployment/web web="$DEPLOY_IMAGE"
kubectl -n storefront rollout status deployment/web --timeout=180s
kubectl -n storefront get pods -l app=storefront-web \
  -o custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,IMAGE:.spec.containers[0].image
curl --fail --show-error --retry 3 https://shop.example.com/up
kubectl -n storefront rollout history deployment/web

Jeśli readiness albo publiczny test zawiodą, zatrzymaj pipeline i zbierz Events, opis podów oraz logi przed zmianą kolejnej zmiennej. Wycofaj Deployment dopiero po potwierdzeniu, że migracja pozostaje kompatybilna z poprzednim obrazem:

sh
kubectl -n storefront rollout undo deployment/web
kubectl -n storefront rollout status deployment/web --timeout=180s
curl --fail --show-error https://shop.example.com/up

rollout undo zmienia szablon poda; nie odwraca Job bazy danych. To główny powód zmian expand-contract i unikania destrukcyjnych migracji danych w tym samym release co zmiana web. Gdy schemat nie jest kompatybilny wstecz, incydent wymaga świadomego planu odzyskania, nie optymistycznego polecenia Kubernetes.

Kiedy ten pipeline jest za duży — albo za mały

Dla jednej aplikacji z rzadkimi release'ami chroniony ręczny skrypt deployu, który zapisuje SHA obrazu, może być bezpieczniejszy niż niedokończony system CI/CD. Alternatywą nie jest klikanie na serwerze: to mała, reviewowana, powtarzalna procedura z tym samym artefaktem i regułami weryfikacji. Z drugiej strony ten pipeline ograniczony do namespace nie wystarczy dla promocji multi-cluster, rekonsyliacji GitOps, podpisanych artefaktów ani regulowanego change control. Te potrzeby mogą uzasadnić Argo CD, Flux, kontroler deployu, podpisywanie obrazów i osobny projekt audytu.

Nie używaj CI jako doraźnego dostępu shell do produkcji. Procedura awaryjna może być potrzebna, lecz musi zostawić ślad audytowy i zostać uzgodniona z Gitem; inaczej kolejny deklaratywny release usunie niewyjaśnioną naprawę. Następna część zamienia sygnały pipeline w rutynowe kontrole operacyjne, dowody backupu i ćwiczenia incydentów.

Najważniejszy test tego procesu nie brzmi „czy dzisiejszy deploy przeszedł?”, lecz „czy inna osoba potrafi odtworzyć jego decyzje za miesiąc?”. Powinna znaleźć commit, digest obrazu, wynik migracji, zmianę Deployment, status rolloutu i wynik testu publicznego bez logowania się na serwer jako root. Jeśli któregoś z tych dowodów brakuje, nie dokładaj od razu kolejnego narzędzia. Najpierw zapisz brakującą informację w pipeline i przećwicz prosty rollback na stagingu. W ten sposób automatyzacja redukuje ryzyko zamiast tylko przyspieszać moment, w którym błąd dotrze do klientów.

Powiązane artykuły

Wsparcie istniejącego systemu

Potrzebujesz pomocy z działającą aplikacją?

Pomagam firmom rozwijać działające systemy, porządkować wdrożenia i dodawać nowe funkcje bez dokładania chaosu do projektu.

Komentarze (0)
Zaloguj się, aby dodać komentarz

Musisz być zalogowany, aby dodać komentarz.

Zaloguj się

Potrzebujesz kogoś, kto weźmie odpowiedzialność za kolejny krok?

Porozmawiajmy o Twoim projekcie i określmy zakres, który ma sens dla Twoich celów.