Модуль 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-by-convention 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 решения сформулированы без преувеличений.