Недавно я столкнулся с ситуацией, когда у нас возник конфликт IP-адресов с кластером Kubernetes другой команды, где у них был блок CIDR сети pod, который конфликтовал с блоком CIDR VPC, в котором находился мой кластер (а также устаревшие экземпляры EC2). Кластер моей команды могли разговаривать со своим кластером через пиринг VPC, но они не могли разговаривать со мной таким же образом. Мы не хотели размещать входящие данные приложений в общедоступном Интернете, и из-за внутренних ограничений мы не могли расширить блок CIDR моего VPC. Единственное решение, которое можно было найти, - это настроить VPC с другим блоком CIDR. Это достаточно просто для экземпляров EC2 за пределами кластера Kubernetes, но миграция кластера без простоев в реальном времени была непростой задачей. Благодаря конвейеру развертывания приложений кластеры стали домашними животными для инженерных групп. Это создает ряд проблем, при которых развертывание и миграция в другой кластер невозможны без значительных временных затрат многих команд. Выполнение этой миграции без простоев казалось сложной задачей, но, поскольку объем был определен, это могло стать разумной целью.

ПРИМЕЧАНИЕ. В последний момент было решено не выполнять этот перенос. Шаги, которые я описываю ниже, были выполнены несколько раз в лабораторной среде без проблем, но так и не перешли на стадию производственной миграции. :(

Не вся работа попадает в производство, и это был один из таких случаев. Однако я все же считаю, что этим процессом стоит поделиться. :)

Как выглядел кластер

Я большой поклонник инструментов Hashicorp Инфраструктура как код, и они хорошо работают в моей среде. Для развертывания кластера я создаю AMI с помощью Packer, развертываю его в AWS AutoScalingGroup (s) с помощью Terraform, а затем запускаю собственный скрипт Python для циклического переключения узлов. Код Terraform состоит из 3 основных компонентов; модуль (worker-common) для общих ресурсов (группы безопасности, LB, DNS и т. д.), модуль (control-plane) для уровня управления и модуль (worker-{pool}) для рабочих узлов. Существует около 72 AutoScalingGroup (по одной на каждый тип инстанса на каждую зону доступности плюс по требованию по сравнению с местом), около 200 рабочих узлов и многие тысячи подов, работающих в кластере, который мне нужно было перенести.

Код Terraform по сути выглядел так:

Проблемы

В Terraform конфигурация декларативна, и из-за допущений, сделанных в коде, мне пришлось решить несколько проблем.

  • Не может дублировать имена для InstanceProfiles, AutoScalingGroups и LoadBalancers.
  • Невозможно зарегистрировать экземпляры в TargetGroup, которая находится в другом VPC.
  • SecurityGroups нельзя использовать в VPC; хотя на них можно ссылаться в правилах
  • Записи DNS были CNAME для LB, а не A-записями.

Пошаговые инструкции по решению

Я разбил проблему на части и решил каждую индивидуально. Основной порядок действий был примерно таким. Я рассмотрю каждый фрагмент ниже.

  1. Создайте новый VPC
  2. Настройте общие ресурсы в обоих VPC
  3. Создайте дополнительные рабочие узлы в новых VPC
  4. Маршрутизация трафика к обоим VPC
  5. Направляйте трафик только в новый VPC
  6. Перенести рабочие нагрузки на узлы в новом VPC
  7. Перенести узлы уровня управления в новый VPC
  8. Очистите старые ресурсы

Опять же, это кажется большим делом, но оказалось, что это не так много, как я первоначально думал. Одна из вещей, которые упростили эту задачу, - это богатый набор тегов на всех наших ресурсах AWS. Я смог четко сослаться на них в коде Terraform и соответствующим образом использовать ресурсы данных.

Создание нового VPC

Первым делом было создание нового VPC. Это было довольно просто, за исключением придумать имя, которое всем понравилось. Оба VPC были подключены друг к другу, чтобы все могли разговаривать друг с другом конфиденциально.

Настройка общих ресурсов

Как только это было создано, я начал заниматься шаблоном именования ресурсов, и это тоже оказалось довольно легко решить. Я добавил новый параметр в модуль worker-common Terraform, который позволил мне установить суффикс для каждого ресурса. По умолчанию переменная была пустой, поэтому префикс не добавлялся и существующие ресурсы не затрагивались. Затем я мог бы настроить модуль worker-common-migration, который создает новые ресурсы в новом VPC. Поскольку мне не нужно было перемещать InstanceProfile (он не зависит от VPC), я добавил флаг, указывающий, должен ли код создавать его.

Я применил небольшую хитрость для SecurityGroups, чтобы во время миграции «общая» SecurityGroup ссылалась как на старый, так и на новый идентификаторы. Это возможно, потому что новый VPC связан со старым VPC.

Теперь я мог запустить Terraform, чтобы LBs и SecurityGroups создавались в новом VPC. После завершения запуска Terraform новые «общие» ресурсы существуют, но их еще никто не использует.

Рабочие узлы

Следующим шагом было создание рабочих узлов в новом VPC. В отличие от worker-common, мне не нужно было создавать модуль миграции для рабочих. Мне просто нужно было добавить новый (x24!), Который ссылался бы на вновь созданные ресурсы.

Прежде чем я смог запустить terraform apply, мне нужно было обновить контроллер Ingress. Мне пришлось изменить Службу на externalTrafficPolicy: Cluster, чтобы, когда контроллер входящего трафика начал работать на узлах в новом VPC, трафик по-прежнему направлялся бы к ним. Помните, что я не могу добавить новых воркеров в ту же TargetGroup, что и существующие, потому что они находятся в другом VPC. Это привело к увеличению задержки для каждого запроса через входной контроллер, потому что NodePort должен был быть проксирован, но он был достаточно низким и в течение такого короткого периода времени, что это считалось приемлемым. После того, как новые рабочие были созданы, я добавил помутнение к старым AutoScalingGroups, чтобы на них не было запланировано никаких новых подов.

for node in $(aws ec2 describe-instances --filters "Name=tag:KubernetesCluster,Values=CLUSTER_NAME" "Name=vpc-id,Values=OLD_VPC_ID" | jq -r '.Reservations[].Instances[].PrivateDnsName')l do
    kubectl taint nodes $node migration=migration:NoSchedule
done

Теперь, когда у меня появились новые рабочие в новом VPC, и все они могли общаться друг с другом в обоих VPC, я был готов переключить DNS и указать его на новые LB. На данный момент общее время миграции составило всего ~ 20 минут, и я достигаю цели - нулевое время простоя.

Перенос рабочих нагрузок

На этом этапе мне нужно начать перенос рабочих нагрузок с рабочих в старом VPC на рабочих в новом VPC. Я вручную удалил модули контроллеров Ingress, которые переместили их на рабочих в новом VPC. Это позволило мне вернуться к externalTrafficPolicy: Local и восстановить нормальную задержку входящего трафика. Через пару минут это было сделано, и я смог начать перенос рабочих нагрузок в реальном времени. К счастью для меня, наш сценарий развертывания Python позволяет циклически переключать узлы на основе фильтра меток. Я начал работу, и она начала истощать старые рабочие узлы. Когда каждый узел был опустошен, рабочие нагрузки автоматически переносились только на новые рабочие узлы благодаря добавленному мною заражению. Перемещение рабочих нагрузок с соблюдением бюджетов PodDisruptionBudgets может быть медленным, а для живого кластера ожидалось около 4 часов. При работе с лабораторным кластером этот шаг был выполнен примерно за 30 минут.

Очистка кода

Пока мне нужно было перенести контрольную плоскость, я хотел очистить код Terraform, чтобы начать удаление дуэльных модулей. Я перевернул модуль worker-common-migration на использование create_instance_profile = "true" и сделал обратное в исходном модуле worker-common. Затем я переместил ресурсы в состоянии Terraform из одного модуля в другой.

terraform state mv module.worker-common.aws_iam_instance_profile.this module.worker-common-migration.aws_iam_instance_profile.this
terraform state mv module.worker-common.aws_iam_role.readonly module.worker-common-migration.aws_iam_role.readonly
terraform state mv module.worker-common.aws_iam_role.this module.worker-common-migration.aws_iam_role.this
terraform state mv module.worker-common.aws_iam_role_policy.this module.worker-common-migration.aws_iam_role_policy.this
# etc, etc

Я указал всем рабочим модулям на вывод нового модуля миграции с помощью sed.

find . -type f -name "*.tf" -not -path '*/\.terraform' -exec gsed -i 's/module.worker-common.instance_profile_id/module.worker-common-migration.instance_profile_id/g' {} +

Я удалил исходный worker-common.tf и изменил источник модуля, на который указывал worker-common-migration.tf.

module "worker-common-migration" {
  source = "../modules/worker-common"
  # the rest of the code is the same
}

Несмотря на то, что модуль теперь назывался worker-common-migration, код, который он использует, теперь такой же, как и у всех других кластеров. Параметры по-прежнему указывают на новый VPC, но используемый код тот же, и это важная часть для будущей разработки и обслуживания. При следующем запуске Terraform были удалены старые AutoScalingGroups и SecurityGroups, поскольку они больше не нужны. Теперь все, что осталось, это сделать плоскость управления.

Плоскость управления

С плоскостью управления все усложняется. У меня не может быть более одного узла, использующего один и тот же том etcd и одновременно записывающих на него. Это означает, что мне придется остановить один из узлов, воссоздать его в новом VPC, а затем запустить его, когда другие узлы все еще работают. Я могу делать только по одному, если не хочу терять кворум на etcd. Из-за того, что группы безопасности уровня управления создавались внутри того же модуля, который создает узлы уровня управления, я не смог проделать тот же трюк, что и с рабочими. Вместо этого я использовал переменную с именем extra_security_groups, которую можно было использовать для присоединения дополнительной SecurityGroup к узлам плоскости управления. Я нарушил правило и вручную создал группу SecurityGroup в существующем VPC с такими же правилами и вручную прикрепил ее к каждому узлу уровня управления. Это означало, что теперь Terraform может безопасно удалить исходную группу SecurityGroup и воссоздать ее в новом VPC.

Затем код, вызывающий модуль, был обновлен, чтобы указывать на новый VPC и подсети (например, data.aws_subnet.private-migration.*.id). Вместо общего terraform apply мне нужно было запускать каждый шаг миграции, используя -target флаг Terraform для ресурсов, которые я хотел перенести в первую очередь.

terraform apply -target module.control-plane.aws_security_group.control-plane -target module.control-plane.aws_security_group_rule.control-plane-egress -target ... -target ... # etc etc

Однако я столкнулся с проблемой LB, который работает перед узлами уровня управления. Мне нужно было иметь возможность балансировать между двумя разными VPC, а это невозможно с TargetGroup. Управление записями DNS и их изменение в нужное время во время миграции было затруднено с настройкой нашего кода, поэтому я решил нарушить другое правило и вручную обновил запись DNS. Я изменил его, указав на LB, и теперь он должен быть A-записями первого узла уровня управления в новом VPC. Это позволило мне поддерживать полную безотказную работу при вызовах API, сделанных извне кластера.

Я удалил параметр extra_security_groups и запустил другое целевое приложение, чтобы воссоздать только один набор ресурсов уровня управления.

terraform apply -target module.control-plane.aws_autoscaling_group.this.2 -target module.control-plane.aws_autoscaling_group.etcd.2 -target module.control-plane.aws_launch_template.this.2 -target module.control-plane.data.template_file.user_data.2 # etc etc

В результате у меня осталась одна часть плоскости управления в новом VPC, а остальная часть - в старой. Теперь, когда у меня была часть плоскости управления, работающая в новом VPC, я мог безопасно использовать Terraform с помощью другой команды -target apply для воссоздания LB в новом VPC. В нем будет только один узел apiserver, но это нормально, потому что количество внешних вызовов Kubernetes API довольно мало, и он может справиться с нагрузкой. Внутренние вызовы используют службу kubernetes.default, и на них эти изменения не влияют. После воссоздания LB я смог переключить DNS обратно на конфигурацию, которая является CNAME для LB.

Я снова запустил terraform apply -target ... -target ... для следующей части плоскости управления, и она тоже была перемещена. Промойте и повторите еще раз, и уровень управления будет запущен в новом VPC!

Миграция уровня управления была намного более ручной, чем я предпочитаю, но она выполнялась примерно за 30 минут. Большую часть этого времени мы ждали, когда ресурсы AWS будут созданы и подключатся к сети. В целом, из-за того, как мы создаем наши AMI, с момента начала загрузки и подготовки узла в кластере Kubernetes до готовности узла проходит 4–7 минут.

Больше очистки

На этом этапе кластер полностью перенесен, но код был беспорядочным. Я сделал еще один проход через код и избавился от всего data.aws_subnet.private-migration кода. Он был обновлен, чтобы использовать новый VPC только при поиске, и ссылки были возвращены к исходному data.aws_subnet.private. worker-common-migration имя модуля в приложении всегда будет там. Что ж ... если только это не начнет меня слишком сильно беспокоить, и я выполняю все команды terraform state mv, чтобы переместить его, но это похоже на большой риск, не имеющий реальной ценности.

Однако все AutoScalingGroup по-прежнему имеют имена с суффиксом -migration, и это может сбивать с толку, если кто-то просматривает ресурсы AWS. Я настраиваю другой набор рабочих без asg_suffix и устанавливаю заражение на тех, с -migration так же, как я делал это во время миграции. Все новые рабочие нагрузки теперь переходят в эти новые ASG и постепенно истощают старые с течением времени или всякий раз, когда будет выпущено следующее развертывание; нет никакой пользы от повторного включения кластера сейчас. Когда все рабочие нагрузки выйдут за -migration уровень, я также удалю этот код из Terraform.

Последние мысли

Короче говоря, это была огромная боль, но я рад, что мне пришлось это сделать. Я разочарован тем, что он так и не дошел до производства, но иногда так бывает. Думаю, я узнал больше о том, как работает код, чем когда-либо писал его. Звучит странно, но это правда. Когда я писал большую часть исходного кода, мне приходилось думать о том, как вещи связаны друг с другом, но за 2 года, прошедшие с тех пор, мне ни разу не приходилось об этом думать.

Я с нетерпением жду, когда IPv6 появится в наших VPC и центрах обработки данных. Предположим, вы не сделаете что-то ... уникальное ... что оставит конфликты IP-адресов в прошлом. Я не уверен, когда мы это сделаем, но, к счастью, Kubernetes сделал это возможным, когда придет время.