Kubernetes 1.1 в продакшне: namespace-изоляция для dev/staging/prod на одном кластере
Запустили первый k8s-кластер для клиента: namespace-политики разделили окружения, autoscaling заработал из коробки. YAML-шаблоны и уроки первого дня.
Kubernetes 1.1 стабильно работает в продакшне, команда анонсировала 1.2 с улучшенным autoscaling и ingress - оркестрация контейнеров быстро взрослеет
Последние несколько месяцев мы смотрели на Kubernetes с позиции людей, которые читают доки и гоняют кластер на виртуалках. В середине января сделали следующий шаг: подняли первый рабочий k8s-кластер для внутреннего сервиса клиента на сопровождении. Клиентский, не наш тестовый - это другое ощущение ответственности.
Задача была конкретная: один кластер, три окружения - dev, staging, prod. Раньше это решалось тремя отдельными VM на каждое окружение, ручным rsync-ом конфигов и вечным вопросом «а мы точно деплоимся на staging а не на прод?». Kubernetes namespace-ы предлагали решение, и мы решили проверить его в реальных условиях.
Почему 1.1, а не ждать 1.2
Kubernetes 1.2 анонсировали в конце 2015-го и он обещает интересные вещи - улучшенный ingress-контроллер, более зрелый autoscaling. Но 1.1 вышел ещё в ноябре, успел обрасти bugfix-релизами, и на нём уже есть production-истории от нескольких компаний. Для первого боевого кластера брать что-то свежее из trunk-а показалось неразумным. Взяли 1.1.7 - последний патч на тот момент.
Namespace как граница окружения
Базовая идея: один namespace - одно окружение. Выглядит просто, но работает нетривиально, потому что namespace в Kubernetes - это не просто папка с ресурсами. Это граница для:
- Квот (
ResourceQuota). Можно ограничить, сколько CPU и памяти может съесть всё, что живёт в dev, - и не бояться что тестировщик задеплоит 50 реплик и убьёт prod. - RBAC (точнее, пока его прообраза). Права на
kubectl applyв dev не дают права в prod. В 1.1 политики авторизации ещё плоские и через аннотации, но разграничение работает. - Network policy. Поды из разных namespace-ов по умолчанию не изолированы на сетевом уровне - это надо помнить и добавлять политики явно, если нужна реальная изоляция.
Третий пункт нас немного удивил. Логично ожидать, что dev не может достучаться до prod по сети - но это не так из коробки. Настраивается через NetworkPolicy, но в 1.1 поддержка сетевых политик зависит от CNI-плагина. Мы используем Flannel - и с ним network policy не работает. Для нашего случая это приемлемо: внутренний сервис, доступ к prod-namespace регулируется на уровне RBAC, а не сети. Но знать об этом надо заранее.
Структура манифестов
Мы организовали YAML-шаблоны так: один базовый набор для приложения, три overlay-директории для окружений. Helm не используем - просто переменные в именах файлов и параметризация через ConfigMap.
Namespace для prod:
apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
env: production
ResourceQuota на namespace:
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: dev
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "20"
Деплоймент с явным указанием namespace и resource requests - без них планировщик будет размещать поды случайно, и потом не понять почему нода задыхается:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: app-api
namespace: prod
spec:
replicas: 2
template:
metadata:
labels:
app: api
env: prod
spec:
containers:
- name: api
image: registry.internal/app-api:1.4.2
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
Горизонтальный autoscaling
HPA (HorizontalPodAutoscaler) в 1.1 работает по CPU - только по нему, не по памяти и не по кастомным метрикам. Для большинства stateless-сервисов этого хватает. Настраивается просто:
apiVersion: extensions/v1beta1
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: extensions/v1beta1
kind: Deployment
name: app-api
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
Сработало в первый же день - под нагрузочным тестом кластер поднял дополнительные реплики без нашего участия. Это приятно.
Что реально удивило в первый день
Несколько вещей, которые в доках написаны мелким шрифтом или не написаны вообще.
Первое - kubectl context. Работать без явного указания контекста и namespace опасно. После того как мы один раз применили манифест не в тот namespace, завели алиас:
alias kprod='kubectl --context=prod-cluster --namespace=prod'
alias kstage='kubectl --context=prod-cluster --namespace=staging'
alias kdev='kubectl --context=prod-cluster --namespace=dev'
Глупо, но работает.
Второе - image pull policy. По умолчанию IfNotPresent. Если на ноде уже лежит образ с тегом latest - новый деплой его не перетянет. В dev, где все гоняют образ с тегом dev-latest, это источник боли. Либо использовать уникальные теги (хеш коммита), либо явно ставить imagePullPolicy: Always. Мы выбрали первый вариант - теги вида 1.4.2-abc1234.
Третье - liveness и readiness пробы. Без них rolling update не знает, что под готов принимать трафик. Мы их поставили - и обнаружили что у одного сервиса healthcheck endpoint возвращал 200 даже когда база недоступна. Пришлось чинить сам сервис, а не k8s-манифест. Kubernetes просто сделал эту проблему видимой.
Текущий статус
Кластер работает третью неделю. Три окружения живут в одном кластере без взаимного влияния на уровне ресурсов - квоты держат. Деплои через CI: Jenkins собирает образ, пушит в registry, меняет тег в манифесте, kubectl apply. Просто и понятно.
Следующий шаг - ingress-контроллер, чтобы не держать отдельный nginx снаружи. Но это уже тема для отдельного поста, когда посмотрим как оно живёт на практике.