Laravel na k3s, część 1: Klaster i pierwsze wdrożenie

8 min. czytaniaZaktualizowano

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

k3s jest lekką dystrybucją Kubernetes odpowiednią dla pojedynczego VPS, gdy potrzebujesz deklaratywnych workloadów, probe'ów i kontrolowanych rolloutów. Nie jest skrótem wokół backupów, planowania pojemności ani administracji Linuxem.

Utwórz namespace, wdrażaj niemutowalne obrazy aplikacji i dodaj readiness oraz liveness probe, zanim wystawisz ruch. Readiness probe decyduje, czy pod otrzymuje ruch; niech będzie tania i zależna wyłącznie od wymagań obsłużenia requestu. Trwałe dane umieszczaj na jawnie wybranym storage i backupuj niezależnie od manifestów. Ingress Traefik kończy TLS i routuje hostname'y; kontener Laravel powinien obsługiwać wyłącznie odpowiedzialność aplikacji.

Pierwszy kamień milowy jest celowo mały: jedno zdrowe wdrożenie, usługa, ingress i udokumentowany rollback przez kubectl rollout undo. Nie dodawaj baz i usług pomocniczych, dopóki ta ścieżka nie jest obserwowalna.

Kiedy k3s jest właściwą granicą

k3s uzasadnia koszt operacyjny, gdy kilka niezależnie wdrażanych workloadów potrzebuje jednego modelu rolloutów, discovery usług i izolacji. Pojedynczy kontener Laravel z zarządzaną bazą często będzie prostszy na Docker Compose lub platformie zarządzanej. Kubernetes nie czyni jedno-węzłowego VPS wysokodostępnym: węzeł, dysk i sieć nadal są jednym punktem awarii. Wybieraj go dla powtarzalnej platformy, nie dlatego, że manifest wygląda bardziej profesjonalnie.

Załóż Ubuntu VPS z firewallem otwierającym tylko SSH i HTTPS. Zainstaluj k3s z oficjalnej dokumentacji wydania, chroń wygenerowany kubeconfig i od razu sprawdź węzeł — poprawny kod wyjścia instalatora nie jest dowodem działania:

sh
curl -sfL https://get.k3s.io | sh -
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl get pods -A
sudo install -m 600 /etc/rancher/k3s/k3s.yaml /root/kubeconfig.yaml

Nie wklejaj kubeconfigu do CI ani nie commituj go. Później CI dostanie token dedykowanego service accountu z wąsko ograniczoną rolą. Teraz utwórz namespace aplikacji i niemutowalną referencję obrazu. registry.example.com/acme/store:3f18d7c musi wskazywać obraz, który już istnieje; latest nie daje prawdziwego rollbacku.

Mały, ale poprawny workload

Obraz poniżej wystawia trasę health Laravel bez wołania zewnętrznego dostawcy płatności. Readiness dowodzi, że pod może przyjąć ruch; liveness jedynie, że proces PHP nie utknął. Nie każ liveness odpytwać bazy: awaria bazy ma odebrać ruch przez readiness, a nie restartować wszystkie zdrowe procesy web.

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: storefront
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: storefront
spec:
  replicas: 2
  selector:
    matchLabels: { app: storefront-web }
  template:
    metadata:
      labels: { app: storefront-web }
    spec:
      containers:
        - name: web
          image: registry.example.com/acme/store:3f18d7c
          imagePullPolicy: IfNotPresent
          ports: [{ containerPort: 8080, name: http }]
          envFrom: [{ secretRef: { name: storefront-runtime } }]
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits: { cpu: 500m, memory: 512Mi }
          readinessProbe:
            httpGet: { path: /up, port: http }
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /up, port: http }
            initialDelaySeconds: 20
            periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata: { name: web, namespace: storefront }
spec:
  selector: { app: storefront-web }
  ports: [{ port: 80, targetPort: http, name: http }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: storefront
  namespace: storefront
spec:
  ingressClassName: traefik
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend: { service: { name: web, port: { name: http } } }

Wdróż i obserwuj rollout, a nie tylko odpowiedź API:

sh
kubectl apply -f k8s/base.yaml
kubectl -n storefront rollout status deployment/web --timeout=120s
kubectl -n storefront get pods,svc,ingress
kubectl -n storefront describe deployment/web

envFrom jest celowo referencją, nie manifestem sekretu. Twórz wartości runtime poza Gitem przez manager sekretów albo chroniony krok wdrożenia. Sekrety Kubernetes są domyślnie tylko kodowane base64, nie są bezpiecznym miejscem dla nieograniczonych poświadczeń produkcyjnych.

Ścieżka awarii przed kolejną funkcją

Udany rollout daje rewizję. Sprawdź rollback, zanim incydent będzie od niego zależał:

sh
kubectl -n storefront rollout history deployment/web
kubectl -n storefront rollout undo deployment/web --to-revision=1
kubectl -n storefront rollout status deployment/web --timeout=120s

Gdy pod pozostaje Pending, obejrzyj kubectl describe pod i events przed zmianą liczby replik. Zwykle winny jest niedostępny obraz, brak sekretu, zły status probe albo requesty większe niż pojemność węzła. Restart ukrywa trop.

Celowo nie wprowadzamy jeszcze MySQL, Redis, Horizon ani uploadów. Usługi stanowe potrzebują historii backupu i odtworzenia, nie samego PVC. Część 2 dodaje śledzalny pipeline, a część 3 — kontrole operacyjne i dowody recovery.

Kontener jest częścią kontraktu

Manifest zakłada, że obraz uruchamia serwer web jako użytkownik niebędący rootem i nie wykonuje php artisan migrate przy starcie procesu. Pod web może zostać odtworzony w każdej chwili; związanie migracji z jego startem powoduje wyścig między replikami i czyni rollback niebezpiecznym. Zbuduj assety i zależności Composera w obrazie, a konfigurację pozostaw środowisku:

dockerfile
FROM php:8.5-cli-alpine AS application
WORKDIR /var/www/html
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist --no-interaction --no-scripts
COPY . .
RUN composer dump-autoload --classmap-authoritative --no-dev \
    && addgroup -S laravel && adduser -S laravel -G laravel \
    && chown -R laravel:laravel storage bootstrap/cache
USER laravel
EXPOSE 8080
CMD ["php", "artisan", "octane:start", "--server=frankenphp", "--host=0.0.0.0", "--port=8080"]

To przykład, nie nakaz wdrożenia Octane. PHP-FPM za nginx jest równie poprawne. Liczy się jeden proces na pierwszym planie, udokumentowany port, katalogi zapisywalne z minimalnymi potrzebnymi uprawnieniami oraz obraz uruchamialny lokalnie pod tymi samymi nazwami zmiennych. Jeśli aplikacja używa Octane, sprawdź singletony i statyczny stan przed skalowaniem; repliki Kubernetes nie naprawią wycieku stanu między requestami.

Sprawdź ścieżkę ruchu i jej granice

Skieruj DNS dopiero, gdy ingress ma adres, i testuj hostname, nie IP poda. Bezpośredni curl do poda może przejść, choć TLS, dopasowanie hosta lub nagłówki proxy są błędne:

sh
kubectl -n storefront port-forward service/web 8080:80
curl --fail http://127.0.0.1:8080/up
curl --fail --resolve shop.example.com:443:203.0.113.10 https://shop.example.com/up
kubectl -n storefront logs deployment/web --tail=100

Przed przyjęciem ruchu klientów ustaw prawdziwego wystawcę TLS i politykę redirectów; dokładna konfiguracja zależy od tego, czy certyfikatami zarządza Traefik, cert-manager, czy zewnętrzny load balancer. Ustal też harmonogram backupów, patchowania i przeglądu dostępu. Zdrowy deployment jest pierwszą obserwowalną jednostką, nie dowodem disaster recovery ani bezpieczeństwa.

Jawnie określ zachowanie rolloutu

Domyślne ustawienia rolling update łatwo przeoczyć, dopóki mały węzeł nie potrafi zaplanować dodatkowego poda. Zapisz więc kompromis dostępności w Deployment. Przy dwóch replikach maxUnavailable: 0 utrzymuje pojemność podczas normalnej zmiany obrazu, a maxSurge: 1 pozwala na jeden dodatkowy pod tylko wtedy, gdy węzeł ma miejsce na jego requesty zasobów. Na pojedynczym VPS rollout może przez to poczekać, zamiast wyłączyć zdrowego poda. To zwykle bezpieczniejszy sposób porażki.

yaml
spec:
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: web
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 5"]

Te pola dopisz do wcześniejszego Deployment, a nie aplikuj sam fragment. Krótki preStop daje kontrolerowi endpoints chwilę na zaprzestanie kierowania nowych requestów, zanim proces się zakończy. Nie zapewnia trwałości requestów już obsługiwanych. Długie zapytanie, job kolejki albo połączenie WebSocket potrzebują własnego projektu zamykania i timeoutu dopasowanego do aplikacji.

Utwórz wskazywany sekret runtime przez chroniony mechanizm wdrożeniowy. To polecenie jest celowo przykładem dla terminala, w którym wartości dostarcza manager sekretów; nie umieszcza poświadczeń w YAML:

sh
kubectl -n storefront create secret generic storefront-runtime \
  --from-literal=APP_ENV=production \
  --from-literal=APP_KEY="$APP_KEY" \
  --from-literal=DB_CONNECTION=mysql \
  --from-literal=DB_HOST="$DB_HOST" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n storefront get secret storefront-runtime

Nie używaj formy --from-literal w historii shella na współdzielonym bastionie. Wybierz raczej kontroler external-secrets, integrację CI maskującą wartości lub lokalnie zabezpieczony mechanizm wejścia. Pamiętaj też, że aktualizacja Secret nie restartuje podów, które pobrały go jako zmienne środowiskowe. Po rotacji poświadczeń świadomie wymuś rollout i sprawdź, czy nowe pody stają się gotowe.

Runbook pierwszego wdrożenia

Zanim uznasz ten kamień milowy za zakończony, wykonaj te same kontrole, które operator wykona w zły dzień. Potwierdź gotowość każdej repliki, endpoints dla Service i to, że zewnętrzny request trafia do oczekiwanego obrazu. ID obrazu z describe ma znaczenie: HTTP 200 ze starego poda nie oznacza udanego deployu.

sh
kubectl -n storefront get deployment web
kubectl -n storefront get endpoints web
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
kubectl -n storefront get events --sort-by=.lastTimestamp | tail -n 20
curl --fail --show-error https://shop.example.com/up

Gdy weryfikacja nie przejdzie, nie zmieniaj naraz wielu zmiennych. Zapisz obraz Deployment, Events, opis poda i ostatnie logi; potem napraw manifest w Git i zbuduj nowy niemutowalny obraz albo wróć do znanej dobrej rewizji. kubectl rollout undo odwraca wyłącznie szablon Deployment. Nie odtworzy skasowanego PVC, nie cofnie nieodwracalnej migracji ani nie odzyska danych zapisanych przez wadliwy release. Z tego powodu komponenty stanowe i migracje baz danych należą do późniejszego, osobno przećwiczonego kroku.

Kiedy to nie jest właściwe pierwsze wdrożenie

Nie wprowadzaj k3s tylko po to, by uruchomić jedną zwykłą stronę Laravel. Docker Compose z zarządzaną bazą i przetestowaną procedurą backupu jest prostszy w operowaniu, ma mniej uprawnień i często lepiej pasuje do pierwszego systemu produkcyjnego. Zarządzana platforma kontenerowa jest dobrą alternatywą, gdy zespół potrzebuje zarządzanego TLS, autoscalingu i patchowania węzłów bardziej niż API Kubernetes.

k3s staje się sensowną granicą, gdy zespół akceptuje odpowiedzialność za aktualizacje, politykę sieci, retencję obrazów, monitoring węzła i ćwiczenia odzyskiwania — oraz korzysta z wdrażania wielu usług według tego samego kontraktu. Zacznij od tak wąskiego workloadu, aby wybór potwierdzały obserwacje, a nie efektowny diagram klastra.

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.