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.
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.
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.
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ę.
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:
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.
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:
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.