Введение
Теперь вы умеете разворачивать приложения и устранять неполадки, однако клиентам по-прежнему нужен надёжный способ подключаться к ним. IP-адрес Pod временный: Deployment в любой момент может заменить Pod, и новый экземпляр получит другой IP-адрес.
Kubernetes решает эту проблему с помощью Service. Сервис выбирает изменяющуюся группу Pod и предоставляет клиентам единый стабильный сетевой идентификатор. В этой лабораторной работе вы проследите полный путь подключения: от меток до EndpointSlice и кластерного DNS, а также познакомитесь с двумя типами сервисов: ClusterIP для доступа внутри кластера и NodePort для доступа через узел.
В этой лабораторной работе мы намеренно ограничимся сервисами. Ingress добавляет маршрутизацию HTTP и отдельный контроллер; разобраться с ним проще после того, как станут понятны выбор Pod сервисом и доступность приложения.
Разверните серверную часть сервиса
Запуск среды: В этой лабораторной работе для вас запускается полноценный кластер Kubernetes. Настройка плоскости управления, узла и сетевых компонентов обычно занимает 2–3 минуты. Дождитесь полной загрузки среды, прежде чем начинать работу.
На этом шаге вы создадите реплицированное приложение, которое сервисы будут публиковать позднее. Здесь серверная часть — это Pod, способный принимать трафик, направленный сервисом.
Перейдите в рабочий каталог. cd изменяет текущий каталог оболочки; при успешном выполнении команда обычно ничего не выводит:
cd /home/labex/project/service-lab
Создайте Deployment с двумя репликами. Метка Pod app: course-nginx особенно важна, поскольку сервисы будут использовать её как правило выбора.
Синтаксис оболочки cat <<'EOF' > filename называется документом здесь (here-document). Всё до закрывающего EOF записывается в файл, а > создаёт файл или заменяет его содержимое. Кавычки вокруг первого EOF предотвращают случайное раскрытие конструкций оболочки внутри YAML.
cat <<'EOF' > course-nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: course-nginx
spec:
replicas: 2
selector:
matchLabels:
app: course-nginx
template:
metadata:
labels:
app: course-nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
EOF
Примените конфигурацию и дождитесь запуска обоих Pod. Параметр -f указывает apply, из какого файла читать конфигурацию; rollout status ожидает завершения работы контроллера Deployment; --timeout=60s ограничивает время ожидания. В последней команде -l фильтрует объекты по метке, а -o wide добавляет столбцы с IP-адресом Pod и именем узла:
kubectl apply -f course-nginx-deployment.yaml
kubectl rollout status deployment/course-nginx --timeout=60s
kubectl get pods -l app=course-nginx -o wide
Именованный порт http документирует TCP-порт 80 каждого контейнера. В обеих строках должно быть состояние Running, а IP-адреса Pod должны отличаться. Эти IP-адреса действительны, но не являются постоянными адресами для клиентов; на следующих шагах перед ними появится стабильный идентификатор Service.
Свяжите метки с выбором сервисом
На этом шаге вы изучите метаданные, связывающие Service с Pod. Service выбирает Deployment не по имени, а самостоятельно находит Pod, метки которых соответствуют его селектору.
Выведите метки Pod. -l app=course-nginx — это селектор меток, а --show-labels добавляет полный набор меток в последний столбец:
kubectl get pods -l app=course-nginx --show-labels
У каждого Pod есть метка app=course-nginx и автоматически созданная метка pod-template-hash. Сервис должен выбирать только стабильную метку приложения.
Сравните имена, метки и IP-адреса в компактном представлении. -o custom-columns создаёт таблицу из выбранных полей объекта. После каждого заголовка перед : указывается путь к полю, используемому для этого столбца; обратная косая черта переносит одну команду на следующую отображаемую строку:
kubectl get pods -l app=course-nginx \
-o custom-columns='NAME:.metadata.name,LABEL:.metadata.labels.app,IP:.status.podIP,READY:.status.containerStatuses[0].ready'
Сгенерированные имена и IP-адреса позволяют различать отдельные Pod, а общая метка описывает их роль. Именно такой уровень косвенной адресации позволяет сервису продолжать работу после замены одного из Pod.
Убедитесь, что селектор Deployment совпадает с меткой шаблона Pod. -o jsonpath='...' извлекает только запрошенные поля. Текст вне фигурных скобок становится подписью, пути к полям внутри скобок возвращают значения, а {"\n"} вставляет перевод строки:
kubectl get deployment course-nginx \
-o jsonpath='Selector: {.spec.selector.matchLabels.app}{"\n"}Pod label: {.spec.template.metadata.labels.app}{"\n"}'
Оба значения должны быть равны course-nginx. Несовпадение селектора разорвало бы связь контроллера или сервиса с нужными Pod.
Создайте сервис ClusterIP
На этом шаге вы создадите сервис типа ClusterIP, используемого по умолчанию. Он предоставляет виртуальный IP-адрес и DNS-имя, доступные рабочим нагрузкам внутри кластера.
Создайте манифест, используя тот же шаблон с here-document, который был представлен в этой лабораторной работе:
cat <<'EOF' > course-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
EOF
Внимательно изучите сопоставление портов:
port: 80— порт, который клиенты используют на сервисе.targetPort: httpссылается на именованный порт контейнера в каждом выбранном Pod.- Селектор выбирает серверные Pod, а не является сетевым адресом.
Проверьте, примените и изучите сервис. --dry-run=client выполняет локальный разбор без создания объекта; без этого параметра выполняется настоящее применение; затем get service читает активный объект:
kubectl apply --dry-run=client -f course-nginx-service.yaml
kubectl apply -f course-nginx-service.yaml
kubectl get service course-nginx
Значение CLUSTER-IP назначается Kubernetes. Оно остаётся стабильным в течение всего срока существования этого сервиса, даже если его серверные Pod изменятся.
Проследите путь от сервиса до EndpointSlice
На этом шаге вы проследите, как селектор сервиса приводит к фактическим адресам серверов. Kubernetes сохраняет эти адреса в объектах EndpointSlice.
Опишите сервис. Команда describe разворачивает один именованный объект, показывая его конфигурацию, состояние и связанную информацию о конечных точках. Это особенно полезно после краткого вывода get:
kubectl describe service course-nginx
Найдите строку Selector: app=course-nginx и строку Endpoints, содержащую два IP-адреса Pod с портом 80.
Выведите EndpointSlice, выбранный по метке имени сервиса. Селектор -l использует автоматически добавляемую метку kubernetes.io/service-name=course-nginx:
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx
Изучите адреса и их готовность. В этом выражении JSONPath конструкция range повторяет заключённый шаблон для каждой конечной точки. Команда выводит первый адрес, текст ready=, значение готовности и перевод строки:
kubectl get endpointslices -l kubernetes.io/service-name=course-nginx \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
Вы должны увидеть два адреса со значением ready=true. Теперь цепочка выглядит следующим образом:
Service selector -> matching Pod labels -> EndpointSlice addresses -> ready Pods
Если сервис существует, но конечные точки отсутствуют, сначала сравните его селектор с метками и состоянием готовности Pod.
Обратитесь к сервису через кластерный DNS
На этом шаге вы выступите в роли клиента внутри кластера. Кластерный DNS позволяет Pod из того же пространства имён использовать имя сервиса course-nginx, не запоминая его виртуальный IP-адрес.
Запустите временный Pod BusyBox, запросите страницу NGINX и автоматически удалите клиент после завершения команды. Разберём параметры сверху вниз:
--imageвыбирает образ контейнера, а--image-pull-policy=IfNotPresentповторно использует кэшированную копию.--restart=Neverсоздаёт отдельный Pod, а не рабочую нагрузку под управлением контроллера.--rmудаляет Pod после завершения команды, а-iсохраняет подключёнными стандартные потоки ввода и вывода.- Разделитель
--завершает параметры kubectl; всё после него является командой внутри контейнера. wget -qO-тихо запрашивает URL и выводит тело ответа в терминал.
kubectl run service-client \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx
В ответе будет приветственная страница NGINX. Трафик прошёл через сервис, а не напрямую к выбранному IP-адресу Pod.
Выполните более краткую проверку успешного ответа. >/dev/null отбрасывает HTML-код, а && выводит сообщение только в случае успешного выполнения команды запроса:
kubectl run service-client-check \
--image=busybox:1.36 \
--image-pull-policy=IfNotPresent \
--restart=Never \
--rm -i \
-- wget -qO- http://course-nginx >/dev/null && echo "ClusterIP Service responded"
Сообщение об успехе подтверждает и разрешение имени через DNS, и доступность HTTP. Клиентский Pod временный, а сервис и два его серверных Pod остаются запущенными.
Добавьте сервис NodePort
На этом шаге вы создадите второй сервис для тех же Pod, указав тип NodePort. NodePort открывает на каждом узле порт из диапазона 30000–32767 и перенаправляет трафик к серверным Pod сервиса.
Kubernetes может читать несколько объектов из одного много-документного YAML-файла. Каждый объект содержит собственные apiVersion, kind, metadata и spec; строка --- отделяет один YAML-документ от следующего.
Создайте повторно используемый файл, содержащий существующий сервис ClusterIP и новый сервис NodePort. Повторное описание ClusterIP безопасно: применение того же желаемого состояния не изменит объект.
cat <<'EOF' > course-nginx-services.yaml
apiVersion: v1
kind: Service
metadata:
name: course-nginx
spec:
type: ClusterIP
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: course-nginx-nodeport
spec:
type: NodePort
selector:
app: course-nginx
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080
protocol: TCP
EOF
Перед изменением кластера проверьте оба YAML-документа вместе. В выводе должны упоминаться service/course-nginx и service/course-nginx-nodeport, после чего должно следовать (dry run):
kubectl apply --dry-run=client -f course-nginx-services.yaml
Примените конфигурацию и изучите сервисы. Первая команда читает оба документа из одного файла; для существующего сервиса ClusterIP должно появиться состояние unchanged, а сервис NodePort будет создан. Вторая команда читает новый активный сервис:
kubectl apply -f course-nginx-services.yaml
kubectl get service course-nginx-nodeport
В столбце PORT(S) отображается 80:30080/TCP: порт 80 — это порт сервиса, а 30080 — порт, открытый на узле. Оба сервиса выбирают одни и те же Pod, поэтому их серверные адреса могут совпадать.
Сравните границы доступа
На этом шаге вы проверите NodePort и обобщите, когда подходит каждый тип сервиса.
Получите IP-адрес узла Minikube. $(...) — это подстановка команды: оболочка выполняет minikube ip и сохраняет результат в переменной NODE_IP. Параметр -p labex-v135 выбирает подготовленный профиль, а echo позволяет проверить сохранённое значение:
NODE_IP=$(minikube ip -p labex-v135)
echo "$NODE_IP"
Запросите приложение через порт узла. Kubernetes может потребовать несколько секунд, чтобы настроить новое сетевое правило на уровне узла после принятия конфигурации сервиса. Параметры повторных попыток позволяют curl дождаться завершения этого короткого периода вместо немедленного завершения с ошибкой отказа в соединении:
curl -s --retry 5 --retry-connrefused --retry-delay 2 "http://${NODE_IP}:30080" | grep 'Welcome to nginx'
Здесь -s скрывает индикатор выполнения, --retry 5 разрешает до пяти повторных попыток, --retry-connrefused считает ранний отказ в соединении временной ошибкой, допускающей повтор, а --retry-delay 2 ждёт две секунды между попытками. Двойные кавычки позволяют раскрыть ${NODE_IP} внутри URL. Канал | передаёт полученный HTML команде grep, которая выводит совпадающую строку приветствия как подтверждение.
Совпавший заголовок HTML подтверждает, что запрос достиг серверного Pod. Сравните построенные вами пути:
in-cluster Pod -> course-nginx:80 -> ready backend Pod
VM/node client -> NODE_IP:30080 -> course-nginx-nodeport:80 -> ready backend Pod
Изучите оба сервиса одновременно:
kubectl get services course-nginx course-nginx-nodeport
Используйте ClusterIP для стабильного взаимодействия внутри кластера; это тип по умолчанию и распространённая основа для других механизмов публикации приложений. NodePort добавляет точку входа на уровне узла и подходит для обучения, разработки или интеграции с внешними балансировщиками нагрузки. Оба типа зависят от корректных селекторов и готовых EndpointSlice.
В следующем задании вы самостоятельно примените эти идеи, опубликовав несколько веб-нагрузок.
Итоги
Вы создали стабильный сетевой идентификатор перед изменяемыми Pod. Вы связали селекторы Service с метками Pod, проследили выбранные серверные Pod через EndpointSlice, обратились к сервису ClusterIP через кластерный DNS и добавили точку входа NodePort через узел.
Главная модель, которую следует запомнить: Service — это не приложение и не контейнер для Pod. Он постоянно представляет выбранный набор готовых серверных Pod. Если подключение не работает, проверяйте цепочку последовательно: порты сервиса, селектор, метки Pod, EndpointSlice, готовность серверной части и затем границу, через которую подключается клиент.


