Я продолжаю цикл статей по приручению домашнего сервера разработчика, который хочет уметь в DevOps. В первой своей статье я рассказал о развёртывании Xen Project гипервизора и миграции Windows-виртуалок из Hyper-V. Во второй о развёртывании на базе виртуалок этого сервера Kubernetes-кластера. Перед написанием данной я ставил перед собой следующие цели: Развернуть тестовый сайт, состоящий из статических ресурсов и front-end API в vanila Kubernetes-кластере. Обеспечить доступ к этому сайту с использованием NGINX Ingress Controller. Сайт должен быть доступен по HTTPS-протоколу с автоматически обновляемым TLS-сертификатом Let’s Encrypt. Развернуть NGINX ingress Controller довольно просто, однако есть пара моментов. Во-первых, на официальной странице, посвящённой развёртыванию контроллера, в разделе Quick start даётся пример с использованием Helm, пригодный только для облака. Во-вторых, в разделе о развёртывании в bare-metal кластере, сказано, что нужно воспользоваться готовым YAML-файлом, специально предназначенным для такого случая. На самом деле для развёртывания контроллера и в облаке и на железе можно и нужно использовать helm-chart, но перед этим нужно разобраться с его параметрами (values), коих более 300. Не вполне полная документация по ним дана на странице git-репозитория контроллера. В файле значений по умолчанию также есть масса полезных комментариев. Первое, что нужно сделать, чтобы получить возможность установить наш ingress-контроллер, это добавить его репозиторий в helm-клиент с помощью следующих команд: Далее необходимо создать файл nginx-values.yaml. Для моего случая получилось следующее: Далее я поясню значение этих настроек. Так как предполагается использовать в качестве балансировщика нагрузки внешний не управляемый кластером роутер, сервис контроллера нужно публиковать с помощью controller.service.type: NodePort, а не LoadBalancer. Чтобы настроить DNAT на внешнем роутере, необходимо зафиксировать порты для HTTP и HTTPS, прописав их с помощью controller.nodePorts.http: 30100 и controller.nodePorts.https: 30101, соответственно. Сервис с типом NodePort по умолчанию выполняет SNAT, что приводит к тому, что исходный IP в запросах будет соответствовать IP узла Kubernetes-кластера, принявшего запрос. Таким образом, в логах web-приложений IP-адрес присутствовать не будет. Рекомендуемым способом сохранения IP-адреса клиента для сервиса с типом NodePort является установка spec.externalTrafficPolicy в Local. Однако, это означает, что пакеты, полученные узлом, не имеющим запущенного экземпляра ingress controller, будут отброшены. Чтобы избежать этого, на каждом узле кластера, указанном в балансировщике нагрузки роутера (в моём случае worker-узлы), необходимо запустить экземпляр контроллера. Это делается с помощью настройки controller.replicaCount: 2 (по числу worker-узлов). Чтобы гарантировать присутствие пода контроллера на каждом узле, настраивается controller.affinity. В одном кластере одновременно могут использоваться различные продуты, выступающие в качестве ingress-контроллеров. При создании ingress-ресурса можно (и желательно) указать так называемый класс контроллера, соответствующий ingress-продукту (в данном случае nginx == NGINX Ingress Controller). Однако, пользователь может и не указать его. Чтобы кластер работал предсказуемым образом, один из классов ingress-контроллера рекомендуется пометить как используемым по умолчанию. Для этого предназначена настройка controller.ingressClassResource.default: true. Для большей надёжности и для совместимости с ingress-ресурсами, созданными до установки данного ingress-контроллера, также используется возможность NGINX Ingress Controller по отслеживанию ingress-ресурсов без указанного класса ingress-контроллера: controller.watchIngressWithoutClass: true. Выполняем следующую команду: Если вы хотите перед развёртыванием в кластере проанализировать полученные ресурсы, в данную команду можно добавить пару ключей: --dry-run --debug. Развёртывание занимает некоторое время. Чтобы отследить его окончание можно воспользоваться следующей командой, которая завершится по его окончанию: В качестве теста и выявления возможных проблем можно развернуть web-приложение, имитирующее широко распространённый случай: статические файлы SPA + front-end web API. В качестве первого я буду использовать nginx, в качестве второго chentex/go-rest-api. Для начала нам нужно определить необходимые Kubernetes-ресурсы в файле site-sample.yaml: Самым интересным, с точки зрения данной статьи, является последний ресурс, собственно, ingress. В нём я определяю следующее: Класс используемого ingress-контроллера указывает на только что развёрнутого нами NGINX Ingress Controller: ingressClassName: nginx. Ingress будет следить за доступом к сайту es.moysite.ru: host: es.moysite.ru. Для этого ingress-контроллер будет анализировать заголовок host: HTTP-запроса. В рамках этого сайта используются два сервиса: статические файлы SPA, доступные в корневой папке домена (/), и frontend Web API, запросы к которому определяются по префиксу /test. Более подробно работа с ingress-ресурсами описана в статье про Ingress, а описание API на странице документации. Тестовый сайт развёртываем командой: В моём случае я наступил, пожалуй, на большинство «граблей», но для меня это хорошо. Это — опыт. Далее я буду исходить из того, что у меня есть статический публичный IP-адрес, по которому должен быть доступен мой сайт. Адрес сайта es.moysite.ru, соответствие этого имени статическом адресу прописано в публичном DNS-сервисе. Используется роутер, встроенный firewall которого обеспечивает DNAT и SNAT. Я хочу тестировать развёртывание сайта из своей локальной сети. Простейший тест доступности развёрнутого сайта делается с помощью навигации по адресу http://es.moysite.ru в браузере. Если всё хорошо, вы увидите страницу по умолчанию nginx, на которой он поприветствует вас: Welcome to nginx!. Чтобы протестировать доступность симулякра frontend Web API, в браузере пытаемся посмотреть страницу по адресу http://es.moysite.ru/test. Ответом должен быть JSON: Если оба теста прошли удачно, как из локальной сети, так и с устройства, не подключённого к ней, например, мобильника, поздравляю! Однако, как я сказал ранее, у меня так не получилось. Начинаем искать проблему. К любому pod можно обратиться с worker-узла, на котором запущен pod. Для этого, во-первых, необходимо определить worker-узел и адрес самого pod: Вы должны получить что-то подобное: Для примера, я хочу протестировать работоспособность pod spa-deployment-7577c8974d-rh9kw, расположенного на узле worker01 и доступного по адресу 192.168.5.17. Для этого необходимо подключиться во ssh к узлу worker01 и выполнить следующую команду: Если видим кусок HTML с жизнерадостным Welcome to nginx!, идём к следующему шагу. Если нет, начинаем анализировать жизнеспособность pod. Если pod жив и отвечает, тестируем сервис. Сервисы spa и frontend-api имеют тип ClusterIP. Такие сервисы доступны только изнутри кластера. Для проверки их доступности нужно использовать какой-либо pod внутри кластера, например curlimages/curl. Для начала определяем адрес интересующего нас сервиса: Получим примерно такой вывод: Нас интересует IP-адрес сервиса. В данном случае сервис spa внутри кластера доступен по адресу 10.26.111.126. Развёртываем pod curlimages/curl в кластере и подключаемся к его консоли и выполняем команду curl с адресом нашего сервиса: Если всё работает нормально, мы должны увидеть всё тот же кусок HTML с приветствием от nginx. Если что-то пошло не так, начинаем копать под сервис: kubectl describe service spa. Например, в моём случае, я неправильно указал селектор pods в определении сервиса и сервис не был связан ни с одним pod. Заметил я это по отсутствующим IP-адресам в поле Endpoints. Далее можно проверить доступность сервиса по его имени. Для этого в консоли того же curlimages/curl пытаемся обратиться к сервису по его имени: В моём случае я неосторожно поменял какие-то настройки сети уже после установки Calico CNI. В результате DNS в кластере не работал. Это также проявлялось в том, что не работали validation webhooks ingress-контроллера и cert-manager. Решить эту проблему помогла переустановка Calico CNI. В конце не забываем удалить ненужный более pod: Если все предыдущие тесты оказались успешными, пришло время проверить доступность сервисов через ingress-контроллер. Делать это нужно с одного из worker-узлов кластера, в моём случае, например, с worker01. Нужно помнить, во-первых, о том, что HTTP выведен на порт 30100, а также о том, что маршрутизация ведётся по HTTP-заголовку запроса host. Поэтому необходимо указать заголовок host: Как обычно, успешным результатом стоит считать HTML с приветствием nginx. Если есть какие-то проблемы, скорее всего вы что-то напутали с определением ingress-ресурса. Команда для проверки почти та же, что и в предыдущем случае. Я использую IP-адрес 10.44.55.14 (адрес узла worker01), а не его имя, так как DNAT у меня настроен только для подсети 10.44.55.0/24, которая используется для маршрутизации запросов к публичным серверам, а worker01 разыменовывается в подсети 10.44.44.0/24: Если не получили приветствие nginx, значит у вас не настроен или настроен неправильно DNAT и SNAT на вашем роутере. Как это правильно настроить, можно посмотреть, например, в этой статье: Проброс портов и Hairpin NAT в роутерах Mikrotik. Детали будут зависеть от марки и модели роутера, но общие принципы в статье описаны верно. На данный момент мы развернули наш сайт в кластере и можем открыть его, используя HTTP. На самом деле сайт уже доступен по HTTPS, что легко проверить, открыв его в браузере по ссылке https://es.moysite.ru. Правда, по понятным причинам, браузер ругнётся, что сертификат «не торт», так как он действительно самовыпущенный ingress-контроллером. Наша задача заменить этот сертификат на имеющий признанную цепочку доверия. Для того, чтобы использовать HTTPS для вашего сайта, в принципе, достаточно иметь два файла: сертификат и ключ. Содержимое этих файлов сохраняется в кластере в виде secret-ресурса, откуда его использует соответствующий сайту ingress-ресурс. В случае покупных публичных сертификатов вы должны оплатить выпуск этих файлов у сертификационного центра и время от времени обновлять их (обычно раз в год), занося, понятное дело, новую денежку. Let’s Encrypt является центром сертификации, но не берёт с вас денег. Сертификаты Let’s Encrypt нужно обновлять не реже, чем каждые 3 месяца, если я помню правильно. Это тот ещё «геморрой» раз в 3 месяца не забыть запросить новый сертификат и заменить его в кластере. А если сайт не один? Если их несколько десятков? Поэтому появились решения, автоматизирующие этот процесс. Одно из самых известных — cert-manager, cloud native решение для управления сертификатами. cert-manager будет помнить за вас, что сертификаты нужно перевыпустить, обновить соответствующие ресурсы, а всё, что будет нужно от вас, это описать, для каких сайтов нужно выпускать сертификаты, и подключить эти сертификаты, хранящиеся в secret-ресурсах, к соответствующим ingress-ресурсам. Я буду использовать helm-способ развёртывания. Для начала добавляем новый репозиторий Helm: Разворачиваем cert-manager в кластере (installCRDs=true говорит, что нужно автоматически установить дополнительные типы ресурсов в кластер, иначе придётся это делать руками.): Далее я подразумеваю, что у вас есть доменное имя, которое имеет публичную DNS-запись. В моём случае это будет es.moysite.ru. Также стоит отметить, что cert-manager поддерживает два варианта размещения своих ресурсов эмитента и сертификата: в пространствах имён и вне них (cluster wide). Я разберу только первый вариант. Cluster wide ресурсы нужны в случае, когда ваш сертификат выпускается на несколько доменов, приложения которых расположены в разных пространствах имён. В kubectl командах я подразумеваю, что пространство имён test-ingress-app установлено в качестве пространства имён по умолчанию в вашем контексте. У Let’s Encrypt есть два окружения: staging и production. Продуктовый имеет очень строгий лимит на количество запросов. Поэтому, пока вы не будете до конца уверены, что всё у вас работает устойчиво, лучше пользоваться staging окружением. Первое, что нам нужно сделать — это создать ресурс эмитента для staging ресурса в файле lestencrypt-staging.yaml. Важными полями здесь являются: spec.acme.email, в котором нужно указать ваш e-mail адрес, на который будут приходить уведомления об истечении срока действия сертификатов. spc.acme.privateKeySecretRef.name — имя secret-ресурса, в котором будет храниться сертификат, используемый cert-manager. Это не тот же сертификат, который используется нашим сайтом! spec.acme.server указывает на staging окружение. Применяем этот файл: Проверяем, что был создан issuer: Регистрация учётки занимает некоторое время, поэтому поначалу READY будет False. Если False держится более 2 минут, стоит посмотреть, что с ним не так с помощью команды: kubectl describe issuer. В поле Events можно посмотреть в чём проблема. Также для диагностики проблем полезно посмотреть на ресурсы типов certificaterequests и challenges. В моём случае была проблема с настройкой роутера. Во-первых, нужно пробросить порты 80 и 443 с WAN-интерфейса роутера на порты 30100 и 30101 соответственно, для чего нужно создать DNAT-правила. Во-вторых, разрешить пакеты в цепочке forward к портам 30100 и 30101 worker-узлов кластера с WAN-интерфейса. Более подробно о поиске проблем с cert-manager рассказано в статье Troubleshooting Issuing ACME Certificates. В финале вы должны иметь возможность открыть стартовую страницу сайта в браузере по протоколу HTTPS. Браузер всё также будет ругаться на сертификат, но это ожидаемо, так как мы используем staging-окружение Let’s Encrypt. В качестве эмитента сертификата (см. иконку замочка рядом с адресной строкой) должен быть указан (STAGING) Artificial Apricot R3, а не Kubernetes Ingress Controller, как это было ранее : После того, мы добились работоспособности в staging-окружении, пора перейти на production-окружение Let’s Encrypt. Сделать это довольно просто. Во-первых, создаём issuer-ресурс в файле letsencrypt-production.yaml и применяем его командой kubectl apply -f letsencrypt-production.yaml: Меняем описание ingress-ресурса: Через минуту-две сертификат будет получен и можно будет обновить страницу в браузере. На этот раз никаких предупреждений о плохом сертификате быть на должно, а информация о нём должна выглядеть примерно так: Теперь можно удалить issuer-ресурс и секрет, относящиеся к staging-окружению.