Собственные образы API и frontend «Площадки»
В предыдущем уроке разделили исходники, образ и экземпляр. Теперь подготовим приложения, которыми действительно можно управлять в Kubernetes. У «Площадки» два собственных сервера: API отдаёт JSON, web отдаёт HTML и пересылает запросы к API. Полные файлы находятся в examples/kubernetes-web-lab/api и web; листинги ниже объясняют их устройство, а не предлагают взять неизвестный готовый образ.
Используем стандартные модули Node.js 24, поэтому приложению не требуется установка npm-пакетов. Это уменьшает количество новых понятий в первом Kubernetes-проекте. При этом Docker и базовый образ Node читатель должен подготовить самостоятельно. Автор не выполнял сборку и запуск; приведённые команды описывают последующую локальную работу, а ответы серверов являются ожидаемыми.
HTTP-контракт маленького каталога
Основной маршрут API — GET /api/lessons. Он возвращает приветствие, номер версии, имя Pod и массив уроков. Такой ответ сохраняется на протяжении курса. Когда меняем конфигурацию, увидим другое приветствие; когда обновляем образ, сможем отличить код новой версии; когда масштабируем Deployment, увидим разные имена экземпляров. Один небольшой контракт помогает изучить несколько независимых механизмов.
В полном api/server.mjs обработчик этого маршрута имеет следующий фрагмент:
if (req.method === 'GET' && url.pathname === '/api/lessons') {
return json(res, 200, {
greeting,
version,
pod: os.hostname(),
lessons,
});
}
Здесь json, greeting, version и lessons определены в начале полного файла. Фрагмент не является отдельной программой. Функция json задаёт заголовок UTF-8 и сериализует ответ. Массив уроков остаётся константой: API не принимает команды изменения каталога. Поэтому две копии пока не должны согласовывать записи или владеть общим файлом.
Сервер слушает 0.0.0.0:3000. Это важнее, чем выглядит. Адрес loopback внутри контейнера обозначает сам контейнерный сетевой контекст. Если слушать только 127.0.0.1, соседняя точка доступа может не достучаться до приложения. Запись EXPOSE 3000 в Dockerfile документирует порт, но не меняет адрес прослушивания и не публикует его автоматически на компьютере.
Кроме каталога, API содержит /healthz, /readyz, /startupz и демонстрационный административный маршрут. Пока достаточно знать, что первые пути отвечают за наблюдение состояния. Они не возвращают каталог и не требуют учебного токена. В последующем уроке про probes разделим их смысл, чтобы перезапуск процесса не зависел от случайного пользовательского запроса.
Почему frontend содержит небольшой прокси
HTML использует относительный адрес fetch('/api/lessons'). Браузер отправляет запрос к тому же origin, с которого получил страницу. Он не знает имя Kubernetes Service api и не должен знать внутреннюю топологию. Поэтому web принимает путь /api/ и пересылает его сервером к адресу из API_BASE. Это обычное HTTP-проксирование между двумя приложениями.
В полном web/server.mjs адрес по умолчанию задан так:
const apiBase = new URL(process.env.API_BASE || 'http://api:3000');
Внутри кластера короткое имя api будет разрешаться через DNS Service. Для самостоятельного запуска вне Kubernetes читатель может передать другой API_BASE, например адрес локального API. Такой параметр относится к окружению, а не к скомпилированной браузерной программе. Поэтому смена адреса backend не требует вставлять инфраструктурные имена в HTML.
Прокси ограничивает ожидание upstream пятью секундами и возвращает 502, если API недоступен. Это не повтор запроса и не механизм восстановления API. Разделение полезно для диагностики: web может успешно отдавать страницу, пока обращение к каталогу завершается ошибкой. Интерфейс сообщает об этом через элемент role="status", а список записей обновляется только после корректного JSON-ответа.
В DOM мы используем textContent, а не вставляем названия через innerHTML. Хотя записи сейчас константные, HTTP-данные со временем могут стать внешними. Сохранение текстовой вставки не позволяет названию урока превратиться в HTML-разметку. Для Kubernetes эта деталь несущественна, но учебный проект должен оставаться понятным веб-приложением, а не только объектом инфраструктуры.
Dockerfile и локальные имена образов
Полный Dockerfile API занимает несколько строк:
FROM node:24-alpine
WORKDIR /app
COPY --chown=node:node server.mjs ./
USER node
EXPOSE 3000
CMD ["node", "server.mjs"]
Для web рядом лежит аналогичный файл: он копирует server.mjs и index.html, объявляет порт 8080. Программа запускается обычным пользователем node. Это не полная политика безопасности контейнера, но приложению не требуется административный пользователь для прослушивания непривилегированного порта. Далее манифесты также отключат автоматическое монтирование Kubernetes-токена.
Тег node:24-alpine закрепляет основную ветку, но не конкретное содержимое образа. При повторной сборке в другую дату содержимое базы может отличаться. Перед воспроизводимым выпуском читатель фиксирует доступный digest своей архитектуры. В данном черновике неизвестный digest не подставлен и результат сборки не заявлен. Параметры Dockerfile описаны в официальной справке Docker.
Из корня каталога примера будущая сборка выполняется так:
docker build -t ploshchadka-api:1 ./api
docker build -t ploshchadka-web:1 ./web
Имена ploshchadka-api:1 и ploshchadka-web:1 — локальные имена создаваемых вами образов. Они не означают существование публичного registry с готовым продуктом. После сборки kind понадобится явно загрузить образы в узел. Иначе наличие образа у Docker на компьютере ещё не гарантирует, что контейнерный runtime Kubernetes видит его.
Что должно остаться неизменным при переносе
Полный frontend загружает HTML с диска при старте. Отсутствующий файл поэтому обнаружится при запуске web, а не после случайного обращения пользователя. Копирование всех нужных файлов в образ входит в контракт сборки.
Ожидаемый ответ API содержит два урока, версию 1 и имя процесса. После помещения в контейнер HTTP-контракт должен сохраниться. Путь /api/lessons не превращается в Kubernetes-специфичный URL. Такой подход позволяет диагностировать приложение отдельно от сети кластера: если программа не умеет отвечать сама, Deployment не добавит нужный обработчик.
Типичная ошибка — менять одновременно код, порт и адрес прокси, а затем искать причину недоступности в одном месте. В нашем наборе порт API остаётся 3000, порт web — 8080, а браузерный путь — /api/lessons. При следующем шаге меняется среда исполнения, не контракт. Благодаря этому результат сравнивается с известной моделью.
Проверьте глазами полный проект: у каждого Dockerfile есть все копируемые файлы, у HTML нет ссылки на отсутствующий bundle, у серверов есть обработка ошибок. Исходники не содержат многоточий, скрывающих необходимые определения. Это редакционная подготовка примера; она не равна выполненному запуску. Следующий урок помещает эти собственные образы в локальный кластер и первый Pod.