← К программе курса

Модуль 19 / 19

Финальный проект: production-minded платформа на одном узле

KUBERNETES

Содержание
01

1. Цель финального проекта

Вы спроектируете, развернёте и защитите **production-minded** платформу на одном узле и докажете её поведение acceptance-тестами.

  • декларативна доставка / декларативная доставка
  • service discovery та Ingress / и Ingress
  • security, resources, probes
  • rollout, rollback, disruption
  • metrics, logs, Events та evidence
02

2. Требования и критерии приёмки

  • не менее двух Ready Pod
  • Service имеет два endpoints
  • внутренний и Ingress HTTP возвращают 200
  • ServiceAccount token не монтируется
  • requests, limits и три probes заданы
  • rollout и rollback завершаются
  • удаление одного Pod не прерывает серию запросов
03

3. Границы одноузловой платформы

Две replicas, PDB и rolling update защищают от части workload-отказов, но не от потери VM. Это не HA-кластер. Flannel стенда не является policy engine, поэтому NetworkPolicy здесь проверяет декларацию, а не enforcement.

04

4. Архитектура и методика

Client → Traefik Ingress → Service → EndpointSlice → два Pod.

  • сначала preflight и критерии
  • затем server dry-run, diff и SSA
  • каждое изменение подтверждаем end-to-end
  • после отказа собираем evidence
05

5. Preflight

terminal
kubectl config current-context
kubectl get nodes -o wide
kubectl get --raw='/readyz?verbose'
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top node
kubectl -n traefik-system get deployment,service,pod
kubectl get ingressclass
kubectl get storageclass
06

6. Лаборатория: декларативная платформа

Namespace сначала создаётся отдельно, потому что server dry-run набора YAML не является транзакцией. Манифест объединяет конфигурацию, identity, workload, сеть, disruption budget, autoscaling и security defaults.

terminal
mkdir -p /tmp/k8s-m19
cat > /tmp/k8s-m19/platform.yaml <<'YAML'
apiVersion: v1
kind: Namespace
metadata:
  name: k8s-m19
  labels: {course.goretskiy.pro/project: fundamentals}
---
apiVersion: v1
kind: ServiceAccount
metadata: {name: platform, namespace: k8s-m19}
automountServiceAccountToken: false
---
apiVersion: v1
kind: ConfigMap
metadata: {name: platform-content, namespace: k8s-m19}
data:
  index.html: |
    <!doctype html><html><body><h1>Kubernetes Fundamentals</h1><p>final project is healthy</p></body></html>
---
apiVersion: v1
kind: Secret
metadata: {name: platform-runtime, namespace: k8s-m19}
type: Opaque
stringData: {RELEASE_CHANNEL: stable}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: platform
  namespace: k8s-m19
  labels: {app.kubernetes.io/name: platform}
spec:
  replicas: 2
  revisionHistoryLimit: 3
  strategy: {type: RollingUpdate, rollingUpdate: {maxUnavailable: 0, maxSurge: 1}}
  selector: {matchLabels: {app.kubernetes.io/name: platform}}
  template:
    metadata:
      labels: {app.kubernetes.io/name: platform}
    spec:
      serviceAccountName: platform
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 20
      securityContext: {runAsNonRoot: true, runAsUser: 10001, runAsGroup: 10001, fsGroup: 10001, seccompProfile: {type: RuntimeDefault}}
      containers:
      - name: web
        image: busybox:1.37
        command: ["httpd","-f","-p","8080","-h","/www"]
        envFrom: [{secretRef: {name: platform-runtime}}]
        ports: [{name: http, containerPort: 8080}]
        volumeMounts: [{name: content, mountPath: /www, readOnly: true}]
        resources:
          requests: {cpu: 10m, memory: 8Mi}
          limits: {cpu: 100m, memory: 32Mi}
        startupProbe: {httpGet: {path: /, port: http}, periodSeconds: 2, failureThreshold: 15}
        readinessProbe: {httpGet: {path: /, port: http}, periodSeconds: 3}
        livenessProbe: {httpGet: {path: /, port: http}, periodSeconds: 10}
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities: {drop: ["ALL"]}
      volumes: [{name: content, configMap: {name: platform-content}}]
---
apiVersion: v1
kind: Service
metadata: {name: platform, namespace: k8s-m19}
spec:
  selector: {app.kubernetes.io/name: platform}
  ports: [{name: http, port: 80, targetPort: http}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: platform, namespace: k8s-m19}
spec:
  ingressClassName: traefik
  rules:
  - host: platform.k8s.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend: {service: {name: platform, port: {name: http}}}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: {name: platform, namespace: k8s-m19}
spec:
  minAvailable: 1
  selector: {matchLabels: {app.kubernetes.io/name: platform}}
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: {name: platform, namespace: k8s-m19}
spec:
  scaleTargetRef: {apiVersion: apps/v1, kind: Deployment, name: platform}
  minReplicas: 2
  maxReplicas: 4
  behavior:
    scaleDown: {stabilizationWindowSeconds: 60}
  metrics:
  - type: Resource
    resource:
      name: cpu
      target: {type: Utilization, averageUtilization: 60}
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: {name: platform-ingress, namespace: k8s-m19}
spec:
  podSelector: {matchLabels: {app.kubernetes.io/name: platform}}
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: traefik-system}}
    - podSelector: {}
    ports: [{protocol: TCP, port: 8080}]
YAML
kubectl create namespace k8s-m19 --dry-run=client -o yaml | kubectl apply --server-side --field-manager=course-final -f -
kubectl apply --dry-run=server -f /tmp/k8s-m19/platform.yaml
kubectl diff -f /tmp/k8s-m19/platform.yaml || [ "$?" -eq 1 ]
kubectl apply --server-side --field-manager=course-final -f /tmp/k8s-m19/platform.yaml
kubectl -n k8s-m19 rollout status deployment/platform --timeout=180s
07

7. Проверка состояния и security

terminal
kubectl -n k8s-m19 get deployment,pod,service,endpointslice,ingress,hpa,pdb,networkpolicy -o wide
kubectl -n k8s-m19 get deployment platform -o jsonpath='generation={.metadata.generation}{" observed="}{.status.observedGeneration}{" ready="}{.status.readyReplicas}{" available="}{.status.availableReplicas}{"\n"}'
kubectl -n k8s-m19 describe hpa platform
kubectl -n k8s-m19 top pod --containers
kubectl auth can-i --as=system:serviceaccount:k8s-m19:platform get secrets -n k8s-m19
kubectl -n k8s-m19 exec deployment/platform -- sh -c 'test ! -e /var/run/secrets/kubernetes.io/serviceaccount/token && echo token-not-mounted'
08

8. End-to-end acceptance

terminal
kubectl -n k8s-m19 run client --image=curlimages/curl:8.17.0 --restart=Never --command -- sleep 3600
kubectl -n k8s-m19 wait --for=condition=Ready pod/client --timeout=120s
kubectl -n k8s-m19 exec client -- curl -fsS http://platform
traefik_ip=$(kubectl -n traefik-system get service traefik -o jsonpath='{.spec.clusterIP}')
kubectl -n k8s-m19 exec client -- curl -fsS -H 'Host: platform.k8s.local' "http://$traefik_ip/"
kubectl -n k8s-m19 get endpointslice -l kubernetes.io/service-name=platform
09

9. Release, rollout и rollback

Отдельная immutable-by-convention ConfigMap делает версию конфигурации видимой в Pod template.

terminal
kubectl -n k8s-m19 create configmap platform-content-v2 --from-literal=index.html='<h1>Kubernetes Fundamentals</h1><p>release v2</p>'
kubectl -n k8s-m19 patch deployment platform --type=merge -p '{"spec":{"template":{"spec":{"volumes":[{"name":"content","configMap":{"name":"platform-content-v2"}}]}}}}'
kubectl -n k8s-m19 rollout status deployment/platform --timeout=180s
kubectl -n k8s-m19 rollout history deployment/platform
kubectl -n k8s-m19 exec client -- curl -fsS http://platform
kubectl -n k8s-m19 rollout undo deployment/platform
kubectl -n k8s-m19 rollout status deployment/platform --timeout=180s
kubectl -n k8s-m19 exec client -- curl -fsS http://platform
10

10. Контролируемый отказ

Удалите один Pod и измерьте доступность серией запросов. На одном узле это тест self-healing workload, а не node HA.

terminal
victim=$(kubectl -n k8s-m19 get pod -l app.kubernetes.io/name=platform -o jsonpath='{.items[0].metadata.name}')
kubectl -n k8s-m19 delete pod "$victim" --wait=false
for i in $(seq 1 12); do
  kubectl -n k8s-m19 exec client -- curl -fsS --max-time 2 http://platform >/dev/null && echo "request $i ok" || echo "request $i failed"
  sleep 1
done
kubectl -n k8s-m19 rollout status deployment/platform --timeout=120s
kubectl -n k8s-m19 get pod -l app.kubernetes.io/name=platform -o wide
kubectl -n k8s-m19 get events --sort-by=.metadata.creationTimestamp | tail -25
11

11. Диагностика и evidence

terminal
kubectl -n k8s-m19 get deploy,pod,service,endpointslice,ingress,hpa,pdb
kubectl -n k8s-m19 describe deployment platform
kubectl -n k8s-m19 describe ingress platform
kubectl -n k8s-m19 logs deployment/platform --all-pods=true --prefix --tail=50
kubectl -n k8s-m19 get events --sort-by=.metadata.creationTimestamp
kubectl -n traefik-system logs deployment/traefik --since=10m --tail=80
kubectl get apiservice v1beta1.metrics.k8s.io
kubectl get nodes; kubectl get --raw='/readyz?verbose' | tail -1
12

12. Защита проекта

  • Объясните путь запроса до Pod
  • Докажите связь selector и EndpointSlice
  • Объясните maxUnavailable=0 и PDB
  • Почему HPA нужны requests и Metrics API?
  • Что не переживёт потерю единственного Node?
  • Какой компонент нужен для NetworkPolicy enforcement?
13

13. Cleanup и итог

terminal
kubectl delete namespace k8s-m19 --wait=true
rm -rf /tmp/k8s-m19
kubectl get namespace k8s-m19 2>&1 || true
kubectl get nodes
kubectl get apiservice v1beta1.metrics.k8s.io

Fundamentals завершён, если все acceptance criteria подтверждены фактами, а границы single-node решения сформулированы без преувеличений.