К моему глубочайшему удивлению, начиная с прошлой недели Kubernetes стал неотъемлемой частью моей работы. То есть теперь я не просто должен им интересоваться, его нужно ещё и понимать. А с этим, как можно судить по прошлому Kubernetes посту, есть проблемы. Вроде ж и пример тогда простой был, и по всем шагам прошёлся, но всё равно осталось какое-то ощущение недосказанности.
Я всё думал, почему же так получилось, и, кажется, проблема кроется в неудачном выборе инструментов (а не в мозгах, как мог подумать чрезмерно проницательный читатель). Нюанс в том, что сконфигурировать Kubernetes приложение можно как минимум двумя с половиной способами. Например, есть команды для создания k8s объектов «на лету», вроде kubectl run
или kubectl expose
, которыми я и пользовался в том злосчастном посте. Они простые, понятные, но скрывают пару-тройку важных абстракций, и по итогу картина остаётся неполной.
А оставшиеся полтора способа — это создание объектов из конфигурационных файлов. По одному (первый способ), или сразу пачкой целиком (ещё половина). И, хотя этот подход более громоздкий и менее простой, после него в картине мира вообще не остаётся белых пятен.
Так что сегодня мы снова сделаем что-нибудь простое, вроде реплицированного nginx сервера, но в этот раз каждый объект будет создаваться явно, из файла конфигурации, и полным пониманием того, зачем он нужен.
Предварительная подготовка
Нам понадобятся только VirtualBox, для создания виртуальных машин, minikube, чтобы делать из них Kubernetes кластер, и kubectl, чтобы этот кластер надругивать. Как только всё это установилось, minikube start
сделает свою тёмную магию и Kubernetes кластер заодно.
Pod (язык больше не поворачивается называть его «подом»)
Как вы уже наверное знаете, pod — это самый маленький юнит работы в Kubernetes, обёртка вокруг одного или нескольких контейнеров со своей айпишкой, именем, идентификатором и наверняка богатым внутренним миром.
Как и в прошлый раз, мы сделаем наш первый pod вокруг Docker контейнера с nginx. Только в этот раз — из файла конфигурации:
1 2 3 4 5 6 7 8 |
apiVersion: v1 kind: Pod metadata: name: single-nginx-pod spec: containers: - name: nginx image: nginx |
YAML файлы эстетически и практически прекрасны. Их просто читать, просто создавать. Запороть, правда, тоже легко, но это ко многим вещам относится. В нашем YAML мы задали тип (kind
) создаваемого объекта, имя, и из каких контейнеров он создан. То есть «Pod», «single-nginx-pod» и «nginx» соответственно.
Чтобы создать из него реальный объект, этот файл нужно скормить команде kubectl apply
, и гештальт будет завершён.
1 2 3 4 5 6 7 8 9 10 11 |
kubectl apply -f pod.yml #pod "single-nginx-pod" created kubectl get pods #NAME READY STATUS RESTARTS AGE #single-nginx-pod 0/1 ContainerCreating 0 6s #few seconds later kubectl get pods #NAME READY STATUS RESTARTS AGE #single-nginx-pod 1/1 Running 0 2m |
Pod создался не сразу, ведь nginx образ ещё нужно было и скачать, но через пару секунд всё будет готово. В этот pod можно даже зайти и осмотреться:
1 2 |
kubectl exec -ti single-nginx-pod bash #root@single-nginx-pod:/# |
Смотреть особо не на что, так что я установил htop
, чтобы порадовать себя ядовито зелёными цветами и заодно убедиться, что процесс с nginx
на месте:
Правда, pod-одиночка — уязвим и удивительно бесполезен. Во-первых, снаружи по HTTP к нему не достучаться. Во-вторых, если к контейнеру или хосту придёт костлявая, то, собственно, история nginx на этом и закончится. Если же нам нужно будет его отмасштабировать, то команду apply
придётся повторить ещё кучу раз.
С другой стороны, существуют различные контроллеры, которые как раз и помогают от таких напастей.
Деплоймент-контроллер
Контроллеры — это такие Kubernetes объекты, которые могут манипулировать pod’ами. Например, есть Cron Job контроллер, который умеет запускать их по расписанию. Или ещё есть Replica Set, который может их масштабировать. Но самый универсальный контроллер — это Deployment. Он и воскресить может, и отмасштабировать, и апдэйт накатить, если попросят.
Но сейчас нам важно, чтобы деплоймент следил за здоровьем nginx
, воскрешал его в случае чего, и масштабировал иногда. В общем, создаём.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
apiVersion: apps/v1beta2 kind: Deployment metadata: name: nginx-deployment spec: replicas: 1 selector: matchLabels: app: webserver template: metadata: labels: app: webserver spec: containers: - name: nginx image: nginx |
Эта конфигурация уже по-сложнее. Но, с другой стороны, и объект тоже возмужал. Вот, как выглядит его настройка по частям:
replicas
, разумеется, задаёт, сколько копий pod’а нужно держать живыми.selector
, в свою очередь, как отличать свои pod’ы от чужих. В нашем случае если на pod’е есть меткаapp: webserver
, то клиент — наш.template
— он же шаблон — описывает, каким образом создавать pod, если тот скоропостижно скончался, либо вообще ещё никогда не существовал. Этот кусок очень похож на наш первый YAML код, где мы создавали pod-одиночку. В довесок к описанию контейнера мы повесили на него метку —app: webserver
, чтобы деплоймент мог его найти.
Как и в прошлый раз, kubectl create
сделает всё, что нужно:
1 2 |
kubectl create -f deployment.yml #deployment "nginx-deployment" created |
Через пару секунд мы можем убедиться, что в кластере появилась новая ячейка общества: деплоймент и его pod:
1 2 3 4 5 6 7 8 |
kubectl get deployment #NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE #nginx-deployment 1 1 1 1 2m kubectl get pods #NAME READY STATUS RESTARTS AGE #nginx-deployment-5d789c98df-xchkz 1/1 Running 0 2m #single-nginx-pod 1/1 Running 0 5m |
Поддерживаем заявленную конфигурацию
А теперь смотрите, какой deployment, оказывается, полезный. Представим, что вмешались высшие силы (сегодня это я), и унесли бедолагу-pod’а в лучший из миров (полагаю, это AWS):
1 2 |
kubectl delete pod nginx-deployment-5d789c98df-xchkz # pod "nginx-deployment-5d789c98df-xchkz" deleted |
Ещё до того, как непечатное слово сорвётся с уст возмущённого кластер-администратора, деплоймент всё исправит:
1 2 3 4 5 |
kubectl get pods #NAME READY STATUS RESTARTS AGE #nginx-deployment-5d789c98df-rckfm 1/1 Running 0 4s #nginx-deployment-5d789c98df-xchkz 0/1 Terminating 0 5m #single-nginx-pod 1/1 Running 0 8m |
Масштабирование
Ещё один пример полезности деплоймента — масштабирование вверх и вниз. Если в какой-то момент мы осознали, что наш убийца фейсбука всё-таки «стрельнул», и одним контейнером с nginx больше не отделаешься, за одну команду (либо через YAML) их можно превратить в десяток:
1 2 3 4 5 6 7 8 |
kubectl scale deployment nginx-deployment --replicas=10 #deployment "nginx-deployment" scaled kubectl get pods #NAME READY STATUS RESTARTS AGE #nginx-deployment-5d789c98df-8nn74 1/1 Running 0 8s #8 more pods #nginx-deployment-5d789c98df-rckfm 1/1 Running 0 4m #single-nginx-pod 1/1 Running 0 9m |
Правда, даже после масштабирования у pod’ов с nginx
есть существенный недостаток — к ним всё ещё не достучаться снаружи. Нужна какая-то точка входа.
Сервисы
Точку входа реально тяжело сделать, если pod’ы то умирают, то воскресают, то мигрируют по кластеру. Сервис-объект, в свою очередь, может эти pod’ы отыскать по меткам и общаться с миром от их лица.
Что здорово, сервис может работать даже без pod’ов. Это полезно тогда, когда у нас есть какой-то сервис вне кластера, но мы не хотим, чтобы pod’ы лазили к нему напрямую.
Но сегодня мы сделаем простейший и дефолтный ClusterIP сервис, который позволит общаться к pod’ами по внутренней айпишке:
1 2 3 4 5 6 7 8 9 10 |
apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: webserver ports: - port: 80 targetPort: 80 |
1 2 3 4 |
kubectl get service #NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE #kubernetes ClusterIP 10.0.0.1 <none> 443/TCP 4d #nginx-service ClusterIP 10.0.0.218 <none> 80/TCP 3m |
Бесплатный DNS и балансировщик нагрузки
И хотя внутренняя айпишка на то и внутренняя, что ей снаружи не воспользуешься, у нас появилась пара интересных полезностей. Сервис умеет распределять входящие запросы среди всех своих pod’ов, тем самым работая балансировщиком нагрузки, и кроме этого его имя теперь есть в кластерном DNS, так что с ним можно общаться даже не зная айпишки.
Это легко проверить. Для простоты я уменьшил количество реплик nginx’а до двух и поменял слово «nginx» в их дефолтном HTML на имя хоста. Ну чтобы видеть, с кем общаемся. К тому же у нас всё ещё висит pod-одиночка — single-nginx-pod
, в который можно зайти и попинать наш сервис тем же curl
.
1 2 3 4 |
root@single-nginx-pod:/$ curl -s nginx-service | grep title #<title>Welcome to nginx-deployment-5d789c98df-hsk5z!</title> root@single-nginx-pod:/$ curl -s nginx-service | grep title #<title>Welcome to nginx-deployment-5d789c98df-7mrnt!</title> |
Видели? Это же офигенно. nginx-service
разбрасывает запросы между двумя оставшимися pod’ами и это чётко видно по ответам. То же самое, кстати, умели делать сервисы в Docker Swarm.
1 2 3 4 5 |
kubectl kubectl get pods #NAME READY STATUS RESTARTS AGE #nginx-deployment-5d789c98df-7mrnt 1/1 Running 0 19m #nginx-deployment-5d789c98df-hsk5z 1/1 Running 0 19m #single-nginx-pod 1/1 Running 0 1h |
Но основная проблема-то никуда не ушла! К pod’ам всё не достучаться снаружи. Ладно, это легко исправить.
Ingress
Ингресс — это могущественный чёрный ящик, который может маршрутизировать запросы из внешнего мира к конкретным сервисам внутри кластера. Он умеет делать не только это, но нам сегодня нужно просто провести HTTP запрос к nginx-service. Так что, без лишней болтовни:
1 2 3 4 5 6 7 8 |
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: nginx-ingress spec: backend: serviceName: nginx-service servicePort: 80 |
Если я правильно помню, то сначала ingress ещё нужно было и включить командой minikube addons enable ingress
. Хотя, может, показалось.
После того, как бессменный kubectl create
превратит ingress в нечто большее, чем тыква, мы разузнаем айпишку единственной машины кластера через minikube ip
и начнём любоваться и на дефолтную страницу nginx:
как и на то, что она каждый раз приходит из разного pod’а:
Сильное колдунство.
Мораль
Надеюсь, что теперь всё стало понятнее. Лично мне насоздавать стайку объектов по одному и руками определённо помогло.
Но есть ещё один момент, который не вылазит у меня из головы до сих пор. Если посмотреть на конфигурационный файлы, то, в принципе, их можно создавать в любом порядке. Например, создать сначала сервис, а с pod’ами и деплойментами — подождать. И ничего не упадёт. Оно, конечно, и понятно, объекты же ищут друг друга по селекторам, а не по какой-то жёсткой связке. Но мне интересно, это изначально было частью гуглового плана, или просто они сделали грамотный дизайн, а независимость объектов — его естественный побочный эффект? Вот как они к этому пришли? Интересно же.
Но я отвлёкся. Конфигурационный файлы — трушный путь, Kubernetes теперь классный, двигаемся дальше.
Одно непонятно… Почему вместо создания Ingress мы не можем открыть сервис nginx наружу через NodePort? Для чего в этом случае нужен Ingress?
Мы можем. Если бы мы использовали NodePort или LoadBalancer, то тогда Ingress действительно был бы не нужен. Задачу можно решать по-разному, и здесь мне показалось важным разделить концепции сервиса и внешней точки доступа к нему. Плюс Ingress может работать роутером к нескольким сервисам, и тогда идейно картина была бы вообще прекрасной: все сервисы сидят внутри кластера, а Ingress открывает доступ к тем из них, к которым имеет смысл обращаться снаружи. Сервисам даже не надо в этом случае знать, что существует «большой» интернет.
Понятно. Я просто написал свой конфиг Nginx для всех нужных внутренних сервисов и открыл NodePort наружу. Все работает как положено, с балансировкой. Просто читаю что все для этого используют исключительно Ingress, даже перед Nginx его ставят, вот и не мог понять зачем. По сути, Ingress внутри себя уже содержит Nginx.
Можем! одно из, для того чтобы не светить айпишниками в мир, если у нас к примеру 5 рабочих нод, они могут все ходить через Ингресс (тем неменее через 1 айпи)
Спасибо за статью. В разделе «Сервисы» пропущена команда создания сервиса.
Точно, пропущена. Но там была бы
kubectl create -f
, как и до этого.Что любопытно, самый первый pod я создавал через
kubectl apply
, а деплоймент уже черезkubectl create
. И то и то валидно, но прикольно, что только сейчас заметилСпасибо, отличная статья, помогла разобраться.
Павел, спасибо за прекрасный стиль изложения.
У вас случайно не было проблем чтобы Ingress на external мог уметь вешать серые адреса 10.х.х.х 192.168.х.х. Как-то страшно чтобы ingress во весь мир задом смотрел. Не приходилось для этого чтото дополнительно ‘подпинывать’?
Пожалуйста.
Надеюсь это просто полуночный эффект, а не старость, но начиная с фразы «ingress на external» я вопрос не понял. У нас единственный продакшен кластер живёт в Google Container Engine, и там какую гугл айпишку дал, той и рады. Он жеж гугл. Если хочется перед ingress какой-нибудь reverse proxy или WAF поставить для безопасности, то даже не знаю. В теории, сценарий по умолчанию — оставить ингрес внутри датацентра/проекта/облака, чтобы к нему доступ по внешней айпишке был закрыт (файрволом, либо вешать только на внутреннюю сеть), и в этом же облаке ставить прокси с выходом и наружу, и во внутреннюю сеть c ингрес. Но именно с kubernetes мне такое делать не приходилось.
Спасибо! Лучшая из вводных статей, которые я встречал. Только в приведенной конфигурации ingress у меня отвечал «Connection refused». Отредактировал так:
$cat ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
spec:
rules:
— host: minikube # in /etc/hosts has to be a line ‘ minikube’
http:
paths:
— path: /
backend:
serviceName: nginx-service
servicePort: 80
Пожалуйста! Возможно, мы по-разному кластер настраиваем, или kubernetes уже не торт. Я точно помню, что в мои времена та ingress конфигурация была валидной.