Модуль 19 / 19
Фінальний проєкт: production-minded платформа на одному вузлі
Зміст
1. Мета фінального проєкту
Ви спроєктуєте, розгорнете й захистите **production-minded** платформу на одному вузлі та доведете її поведінку acceptance-тестами.
- декларативна доставка / декларативная доставка
- service discovery та Ingress / и Ingress
- security, resources, probes
- rollout, rollback, disruption
- metrics, logs, Events та evidence
2. Вимоги та критерії приймання
- щонайменше два Ready Pod
- Service має два endpoints
- внутрішній та Ingress HTTP повертають 200
- ServiceAccount token не монтується
- requests, limits і три probes задано
- rollout і rollback завершуються
- видалення одного Pod не перериває серію запитів
3. Межі одноузлової платформи
Дві replicas, PDB та rolling update захищають від частини workload-відмов, але не від втрати VM. Це не HA-кластер. Flannel стенда не є policy engine, тому NetworkPolicy тут перевіряє декларацію, а не enforcement.
4. Архітектура та методика
Client → Traefik Ingress → Service → EndpointSlice → два Pod.
- спочатку preflight і критерії
- потім server dry-run, diff та SSA
- кожну зміну підтверджуємо end-to-end
- після відмови збираємо evidence
5. Preflight
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 storageclass6. Лабораторія: декларативна платформа
Namespace спочатку створюється окремо, бо server dry-run набору YAML не є транзакцією. Маніфест поєднує конфігурацію, identity, workload, мережу, disruption budget, autoscaling і security defaults.
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=180s7. Перевірка стану та security
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'8. End-to-end acceptance
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=platform9. Release, rollout і rollback
Окрема immutable ConfigMap робить версію конфігурації видимою в Pod template.
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://platform10. Контрольована відмова
Видаліть один Pod і виміряйте доступність серією запитів. На одному вузлі це тест self-healing workload, не node HA.
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 -2511. Діагностика та evidence
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 -112. Захист проєкту
- Поясніть шлях запиту до Pod
- Доведіть зв’язок selector та EndpointSlice
- Поясніть maxUnavailable=0 і PDB
- Чому HPA потребує requests і Metrics API?
- Що не переживе втрату єдиного Node?
- Який компонент потрібен для NetworkPolicy enforcement?
13. Cleanup та підсумок
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.ioFundamentals завершено, якщо всі acceptance criteria підтверджені фактами, а межі single-node рішення сформульовані без перебільшень.