ADG Оставить заявку
Блог DevOps 5 мин чтения

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 снаружи. Но это уже тема для отдельного поста, когда посмотрим как оно живёт на практике.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.