Эта статья продолжает наш подробный разбор инфраструктуры Yubo. После рассказа о глобальной архитектуре, организации команды и управлении данными в большом масштабе пришло время сосредоточиться на одном из ключевых столпов нашей платформы — автоматизации в масштабе.
В Yubo мы сделали радикальный выбор: сделать ставку на массовое шаблонирование на базе Custom Resource Definitions (CRD) в Kubernetes, чтобы автоматизировать провижининг, конфигурирование, наблюдаемость и даже полный жизненный цикл машинного обучения.
Такой подход позволяет управлять глобальной инфраструктурой силами небольшой команды, сохраняя при этом строгие гарантии стандартизации, безопасности и масштабируемости.
1. Один кластер, чтобы править всеми
Один из ключевых строительных блоков нашей системы — Argo Workflow.
Мы используем его как движок выполнения для всех наших пайплайнов: от CI до нагрузочного тестирования и провижининга виртуальных машин (кластер Couchbase, реплика-сет MongoDB, кластер Kafka и так далее).
Мы разделили Argo Workflow на несколько инстансов, чтобы команды Yubo (Mobile, Backend, Moderation, Infra и другие) управляли собственными шаблонами workflow. Так проще управлять и RBAC.
Чтобы эффективно управлять этими инстансами, мы шаблонизировали развёртывание Argo Workflow: общая база плюс ресурсы, специфичные для инстанса, — благодаря кастомному плагину ArgoCD на основе Ytt.
Такой подход позволяет каждой команде быть владельцем собственных пайплайнов и предлагать свою библиотеку шаблонов другим командам.
2. CI/CD как шаблон
Начнём с простого — с типового сценария, который есть в каждой компании.
Вместе с нашими разработчиками мы начали с этого простого CI/CD-процесса на основе наших внутренних инструментов: Argo Workflow для CI и ArgoCD для CD.
На самом деле это очень простой CI/CD, но в Yubo сложность не в самом CI/CD, а в том, как масштабировать и поддерживать этот пайплайн такими малыми силами при наших примерно 150 репозиториях с кодом на Node.js, Python, C++, Go или Java и более чем 370 приложениях ArgoCD в продакшене.
Чтобы можно было просто добавить новую нагрузку — на любом языке и в любом дата-центре, — единственно верный, на наш взгляд, путь состоит в том, чтобы шаблонизировать этот пайплайн.
Все наши репозитории с кодом следуют строго одной и той же структуре. Это существенно упрощает шаблонизацию CI, потому что паттерн полностью воспроизводим. Мы полностью запретили себе отклоняться от этого паттерна ради частных случаев.
Использование Argo Workflow действительно позволило нам превращать любой частный случай в новый паттерн, который затем включается в наш шаблон, — и так мы расширяем возможности для наших разработчиков!
Благодаря этой модели провижининг CI/CD для нового сервиса сводится к добавлению его имени в YAML-файл конфигурации.
3. Переход от Terraform к CRD
Когда мы взяли инфраструктуру в свои руки в 2021 году, мы столкнулись с довольно типичной ситуацией: сотни манифестов Kubernetes, вручную скопированных и изменённых, и, конечно же, всё это в одном-единственном кластере Kubernetes.
Terraform знаком всем. Это фантастический инструмент Infrastructure as Code, и нашей первой идеей, разумеется, было заново выстроить инфраструктуру GCP, разделённую на проекты с общим VPC, и управлять всем этим через Terraform.
Именно это мы и сделали, встроив Terraform в наши шаблоны Argo Workflow. Но это ещё не всё! В тот момент мы начали определять, что такое «дата-центр» в понимании Yubo, и в результате создали первый слой шаблонирования вокруг Terraform. Так родился первый CRD от Yubo:
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: datacenters.cloud.yubo.live
spec:
group: cloud.yubo.live
names:
kind: Datacenter
listKind: DatacenterList
plural: datacenters
shortNames:
- dc
singular: datacenter
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
region:
type: string
zone:
type: string
environment:
type: string
network:
type: array
items:
type: object
required:
- configmap
- name
- cluster
- vars
properties:
name:
type: string
configmap:
type: string
cluster:
type: string
vars:
type: object
x-kubernetes-preserve-unknown-fields: true
Мы прогоняли продакшен через этот пайплайн больше года. Со временем нам приходилось менять код и обновлять провайдеры Terraform (ломающие изменения!). С ростом объёма и при размере нашей команды нам становилось всё труднее выдерживать темп и обеспечивать стабильность и функциональность. Более того, каждое изменение требовало запуска пайплайна Terraform:
Ждите подтверждения!
Как знает каждый, кому доводилось работать с Terraform на крупных инфраструктурах, изменения очень быстро становятся нечитаемыми, и малейшая неосторожная ошибка может привести к пересозданию вашего Nodepool.
Так что да, Terraform прекрасен, но явно не подходит под размер нашей инфраструктуры и число рабочих нагрузок в продакшене относительно размера Ops-команды.
Какой же инструмент тогда выбрать?
У нас уже есть CRD, описывающий дата-центр, — так почему бы просто не управлять всей инфраструктурой через CRD? Поэтому мы решили просто шаблонизировать всю нашу инфраструктуру на базе «https://cloud.google.com/config-connector/docs/overview[GCP Config Connector]» точно так же, как шаблонизировали развёртывание Argo Workflow.
Сегодня наша инфраструктура целиком управляется в модели GitOps через ArgoCD и наш плагин «Ytt»:
Ниже приведён фрагмент конфигурации одного из наших дата-центров:
---
env: prod
region: europe-west9
subnets:
- type: back
ipCidrRange: 10.0.0.0/24
secondaryIpRange:
- rangeName: back-pods
ipCidrRange: 10.0.1.0/24
- rangeName: back-services
ipCidrRange: 10.0.2.0/24
- type: front
ipCidrRange: 10.1.0.0/24
secondaryIpRange:
- rangeName: front-pods
ipCidrRange: 10.1.1.0/24
- rangeName: front-services
ipCidrRange: 10.1.2.0/24
internalIps:
- type: back
name: saltstack
address: 10.1.0.250
saltstack:
- name: saltstack-prod
machineType: n2-highcpu-16
ipAddress: 10.1.0.250
image: debian
topics:
- name: lives-scaleup
retention: 86400s
- name: lives-scaledown
retention: 86400s
gke:
- type: front
workloadIdentity: false
defaultMaxPodsPerNode: 32
dnsCacheConfig: false
nodepools:
- name: api
machineType: n2-standard-4
spot: true
podRange: main-pods
enablePrivateNodes: true
initialNodeCount: 6
minNodeCount: 6
maxNodeCount: 300
diskSizeGb: 100
diskType: pd-standard
maxPodsPerNode: 24
labels:
nodepool: api
- name: api-pub
machineType: n2-standard-2
spot: true
podRange: main-pods
enablePrivateNodes: false
initialNodeCount: 10
minNodeCount: 10
maxNodeCount: 30
diskSizeGb: 100
diskType: pd-standard
maxPodsPerNode: 24
labels:
nodepool: api-public
gcsBuckets:
- name: back-prod
type: back
storageClass: STANDARD
lifecycleRules:
- action:
type: Delete
condition:
age: 7
withState: ANY
cloudSQL:
- name: back-prod
version: POSTGRES_15
diskSize: 20
tier: db-custom-1-3840
Добавление нового дата-центра или нового ресурса во все наши дата-центры становится невероятно простым!
Это важное изменение позволило нам повысить скорость работы, снизить затраты на сопровождение кода и, как следствие, повысить общую стабильность платформы.
4. Автоматизация управления нашим Lives-стеком
Пожалуй, одно из моих любимых технических приключений в Yubo.
Наш Lives-стек построен на пулах виртуальных машин, которые регистрируются в Consul с разными тегами в зависимости от используемых ресурсов ВМ и её аптайма. Изначально этот стек работал на bare-metal в дата-центре рядом с европейским магистральным каналом (backbone). Такой сугубо статичный провижининг bare-metal-машин означал, что мы держали серверы, которые попросту простаивали, когда трафик был низким. Чтобы контролировать затраты и масштабируемость этого стека, мы решили перенести его к нашему облачному провайдеру GCP.
Архитектура этого ПО не позволяет использовать MIG для управления scaledown виртуальных машин. Нам нужно гасить машины в несколько этапов, чтобы не отключать и не переподключать всех пользователей одной комнаты одновременно.
Жизненный цикл ВМ мы придумали сами — опять же на основе ресурсов ВМ, аптайма, а также кастомных метрик.
Как мы это сделали? Argo Workflow!
Благодаря нашим моделям развёртывания Argo и управлению ресурсами GCP по модели GitOps на реализацию этого процесса ушло всего несколько минут.
Разумеется, я не буду вдаваться в детали всего стека, но как это работает на практике?
Когда нам нужно больше серверов, мы поднимаем алерт через GCP Cloud Monitoring. Этот алерт отправляется в топик GCP Pub/Sub, который, в свою очередь, читает Argo Events. Этот Pub/Sub и объявление этих алертов, конечно же, описаны в конфигурации наших дата-центров:
---
env: prod
region: europe-west9
topics:
- name: lives-scaleup
retention: 86400s
alerts:
- name: lives-europe-west9-prod
labels:
env: prod
machine_type: n2-highcpu-16
version: v2.1
strategy:
notificationChannelStrategy:
- notificationChannelNames:
- projects/lives-project/notificationChannels/12345678900987654321
renotifyInterval: 1800s
autoClose: 1800s
combiner: OR
conditions:
- conditionThreshold:
aggregations:
- alignmentPeriod: 600s
crossSeriesReducer: REDUCE_MEAN
perSeriesAligner: ALIGN_MAX
comparison: COMPARISON_GT
duration: 0s
filter: resource.type = "gce_instance"
AND metric.type = "compute.googleapis.com/instance/cpu/utilization"
AND (metadata.system_labels.region = "europe-west9"
AND metadata.user_labels.app = "lives"
AND metadata.user_labels.env = "prod")
thresholdValue: 0.70
trigger:
count: 1
displayName: Lives CPU utilization
notificationChannel: projects/lives-project/notificationChannels/12345678900987654321
Затем Argo Events запускает Argo workflow, передавая ему на вход метки нашего GCP-алерта (machine_type, version и так далее), чтобы создать новые виртуальные машины, которые автоматически зарегистрируются в Consul и станут доступны в пуле нашего Lives LoadBalancer.
Та же логика работает и когда трафик снижается и нам нужно убрать серверы. Разница лишь в том, что виртуальные машины мы удаляем в несколько этапов.
Сначала срабатывает первый алерт, который позволяет вывести виртуальную машину из нашего «Lives Load Balancer», навесив на неё специальные метаданные и теги.
Затем, когда на сервере не остаётся ни одной открытой комнаты, поднимается второй алерт и запускается Argo workflow, удаляющий виртуальную машину.
Конечно, здесь описана не вся логика, но это хорошо иллюстрирует, насколько просто всё реализуется благодаря нашей инфраструктуре в модели полного GitOps.
5. Заключение
В Yubo мы сделали своей миссией упростить то, как разработчики взаимодействуют с инфраструктурой. Вместо того чтобы заставлять их погружаться в сложные модули Terraform или разбирать самописные скрипты провижининга, мы даём им понятные декларативные YAML-файлы конфигурации — то, что легко читать, писать и версионировать рядом с кодом приложения.
Этот сдвиг дал огромный эффект. Представляя инфраструктуру в виде чистых шаблонизированных CRD, мы добились того, что:
-
Разработчикам больше не нужно изучать внутреннее устройство Terraform или разбираться в низкоуровневых облачных ресурсах.
-
Они просто описывают свои потребности — базу данных, CI-пайплайн, топик Kafka — в одном YAML-блоке, а обо всём остальном заботится платформа.
-
Всё описывается декларативно, версионируется в Git и применяется автоматически через наши пайплайны GitOps.
Шаблонирование — это не просто оптимизация, это наш способ снизить операционную рутину. Со стандартными паттернами, зашитыми в переиспользуемые шаблоны, мы устранили граничные случаи, сократили время онбординга и радикально снизили издержки на сопровождение инфраструктуры.
Короче говоря: понятные конфиги для разработчиков — меньше работы для инфраструктурной команды. Вот как команда из пяти человек управляет инфраструктурой, обслуживающей десятки миллионов пользователей.
И по мере того как мы распространяем эту модель на ML, комплаенс и безопасность, наша убеждённость только крепнет: самая мощная инфраструктура — та, о которой почти не нужно думать.
Infrastructure as Template (IaT) — вот основа нашей автоматизации!