Приватный Kubernetes за 50 минут

uhcrkro65i5_yo44bofdnzkw3mm.png


Вынести даже часть инфраструктуры из закрытого контура — это ответственный и сложный шаг для многих компаний. Этот момент особенно чувствителен для сфер промышленности, финансов и государственного сектора.

Привет, Хабр! Меня зовут Саша, я ведущий системный администратор в Selectel. В этой статье на примере гибридной инфраструктуры покажу, как развернуть защищенную приватную инсталляцию в облаке, а заодно разберу некоторые мифы. Добро пожаловать под кат!

Используйте навигацию, если не хотите читать текст полностью:

→ Приватные инсталляции Managed Kubernetes
→ Bare Metal Cloud
→ Построение гибридной архитектуры
→ Заключение

Приватные инсталляции Managed Kubernetes


Приватные инсталляции существуют в трех вариантах. Есть приватные хосты, приватные сегменты пула и приватные пулы. Разберем подробнее каждый из них.

Приватный хост


Это изоляция вычислений на уровне физического хоста. Запуск виртуальной машины или воркер-ноды (worker node) Kubernetes происходит на физическом сервере. Он становится приватным хостом, если целиком отдается одному пользователю.

В таком варианте нет конкуренции за вычислительные ресурсы или вероятности, что кто-то скомпрометирует ваши данные. Они изолируются на конкретном хосте, доступ к которому есть только у одного пользователя или компании.

59edded4b3163d9d1b37c91a37e04f62.png


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

Упрощенно это выглядит так: защищаемое приложение может создавать в оперативной памяти анклавы, данные в которых изолируются на физическом хосте. Их не могут прочитать другие приложения, запущенные на том же сервере (включая системные, модули ядра и гипервизор).

b730ee2feed70cf521d8d20105d2da93.png


Приватный сегмент пула


Это группа хостов, к которой можно подключить выделенное сетевое хранилище и хранилище бэкапов. Если для приватного хоста сетевые диски и бэкапы берутся из общего массива, то для приватного сегмента пула они уже включены в конфигурацию. Таким образом, приложение изолируется и на уровне хранения данных. Подробнее о приватных сегментах пула можно прочитать в документации Selectel.

980186755288dc46364d0c3be14524aa.png


Приватный пул


Это выделенные API-ноды, в которых можно изолироваться не только на уровне хостов, вычислений и хранения, но и на уровне сетевой связности, доступа и управляющих API-нод. Этот вариант изоляции хостов максимально близок к уровню изоляции частного облака.

49a5f669a7d31afd8a9fe8cd105955b1.png


as8ay5gc12_myft7wldz1w-8khk.png

Bare Metal Cloud


Это выделенные серверы для высоконагруженных сервисов (интернет-магазинов, игр, SaaS-сервисов, ERP-систем), медиапроектов (сервисов потокового видео, аудио и обработки большого количества пользовательских данных в реальном времени) и проектов в сфере ML и Big Data. В Selectel такие серверы представлены в фиксированной и кастомной конфигурациях.

Серверы фиксированной конфигурации очень похожи на виртуальные машины. Через пару минут после заказа на них уже можно разворачивать Kubernetes. Кастомные серверы собираются под заказ, когда нужна специфическая конфигурация. В Selectel есть свои линии сборки, поэтому срок подготовки сервера — от одного дня.

Построение гибридной архитектуры


Приватный хост — не универсальное решение. Если мы говорим о действительно больших инсталляциях, нужно обеспечить гибридный формат работы и гибридную архитектуру. Допустим, у нас есть задача связать облачную инфраструктуру, включая Managed Kubernetes, с локальной.

Сейчас с помощью Terraform поднимем кластер Kubernetes вместе с приватным Kube API и облачным файрволом. Межсетевым экраном мы закроем кластер от доступа извне, а ходить в него будем через Jump Host, он же Bastion.

Bastion-хост — это хост в сети, который является шлюзом или прокси для всех остальных серверов. Такой хост доступен на внешнем адресе и общается с остальными серверами по приватной сети.


Все манифесты Terraform, которые применяются далее, вы можете найти на GitHub. Используйте их, чтобы реализовать аналогичный проект.

Манифест main


Здесь мы определяем, в первую очередь, переменные, чтобы достать их из Secrets.tfvars — файла с секретами, где хранят пароли и прочую чувствительную информацию. Файл выглядит следующим образом:

domain = "12345"
domain_password = "password"
project_password = "password"


Далее прописываем провайдера (Selectel) и ресурс — наш проект, в котором и будем разворачивать инфраструктуру. Создаем сервисного пользователя и объявляем переменные для провайдера OpenStack.

variable "domain" {
}
variable "domain_password" {
}
variable "project_password" {
}
provider "selectel" {
  domain_name = var.domain
  username    = "webinar"
  password    = var.domain_password
}
resource "selectel_vpc_project_v2" "project_1" {
  name = "webinar"
}
resource "selectel_iam_serviceuser_v1" "serviceuser_1" {
  name     = "project-name"
  password = var.project_password
  role {
    role_name  = "member"
    scope      = "project"
    project_id = selectel_vpc_project_v2.project_1.id
  }
}
provider "openstack" {
  auth_url    = "https://cloud.api.selcloud.ru/identity/v3"
  domain_name = var.domain
  tenant_id   = selectel_vpc_project_v2.project_1.id
  user_name   = selectel_iam_serviceuser_v1.serviceuser_1.name
  password    = selectel_iam_serviceuser_v1.serviceuser_1.password
  region      = "ru-9"
}


Инициализируем Terraform командой terraform init. Если все прошло успешно, идем в раздел Versions и описываем в нем необходимые нам провайдеры и версии для них.

terraform {
  required_providers {
    selectel = {
      source  = "selectel/selectel"
      version = "5.0.2"
    }
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "1.54.1"
    }
  }
}


Возвращаемся в main и запускаем его развертывание:

terraform apply -var-file=./secrets.tfvars


Добавляются два ресурса: проект и сервисный пользователь. Пишем yes и начинаем создавать наш проект.

Когда проект будет готов, увидим в терминале ответ системы вида:»Apply Complete! Ressources: 2 added, 0 changed, 0 destroyed». Теперь идем в панель управления. Проект уже здесь, но пока он пустой. Создадим новый кластер, а заодно посмотрим, какие у нас еще манифесты припасены.

1bc96cad155967214b8a68366da16130.png


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

Манифест network


Следующий шаг — создать сеть. Это также делается предыдущей командой. Посмотрим на манифест network:

resource "openstack_networking_network_v2" "network_1" {
  name           = "private-network"
  admin_state_up = "true"
}
resource "openstack_networking_subnet_v2" "subnet_1" {
  name            = "private-subnet"
  network_id      = openstack_networking_network_v2.network_1.id
  cidr            = "10.222.0.0/16"
  dns_nameservers = ["188.93.16.19", "188.93.17.19"]
  enable_dhcp     = false
}
data "openstack_networking_network_v2" "external_network_1" {
  depends_on = [openstack_networking_network_v2.network_1]
  external   = true
}
resource "openstack_networking_router_v2" "router_1" {
  name                = "router-terraform"
  external_network_id = data.openstack_networking_network_v2.external_network_1.id
}
resource "openstack_networking_router_interface_v2" "router_interface_1" {
  router_id = openstack_networking_router_v2.router_1.id
  subnet_id = openstack_networking_subnet_v2.subnet_1.id
}


network_1 — это приватная сеть, в которой мы будем создавать нашу инфраструктуру. В этой сети нам необходимо создать subnet. Здесь мы отмечаем:

  • cidr, в которой будет «жить» наш кластер Kubernetes и вся остальная инфраструктура;
  • dns_nameservers — публичные рекурсоры Selectel (можно использовать и свои приватные рекурсоры, если они отвечают запросам, которые нужны для кластера);
  • enable dhcp — здесь обязательно указываем значение false, потому что периодически могут возникать проблемы, связанные с работой кластера Kubernetes с включенным DHCP.


В data для external указываем значение true. Также не забываем указать ресурс router, через который будем выходить наружу, и добавить его интерфейс.

И теперь остается запустить развертывание нашего манифеста network. Когда все будет готово, увидим в терминале ответ системы вида »Apply Complete! Ressources: 4 added, 0 changed, 0 destroyed», что подтверждает добавление всех четырех ресурсов. Перейдем к следующим манифестам.

Манифест cloud-firewall


Сам манифест выглядит следующим образом:

resource "openstack_fw_policy_v2" "firewall_policy_1" {
  name    = "ingress-firewall-policy"
  audited = true
  rules = [
    openstack_fw_rule_v2.rule_1.id,
    openstack_fw_rule_v2.rule_2.id,
    openstack_fw_rule_v2.rule_3.id,
    openstack_fw_rule_v2.rule_4.id,
    openstack_fw_rule_v2.rule_5.id,
    openstack_fw_rule_v2.rule_6.id,
  ]
}
resource "openstack_fw_rule_v2" "rule_1" {
  name              = "allow-udp-188.93.16.19-53"
  action            = "allow"
  protocol          = "udp"
  source_ip_address = "188.93.16.19"
  source_port       = "53"
}
resource "openstack_fw_rule_v2" "rule_2" {
  name              = "allow-udp-188.93.17.19-53"
  action            = "allow"
  protocol          = "udp"
  source_ip_address = "188.93.17.19"
  source_port       = "53"
}
resource "openstack_fw_rule_v2" "rule_3" {
  name              = "allow-udp-92.53.68.16-443"
  action            = "allow"
  protocol          = "tcp"
  source_ip_address = "92.53.68.16"
  source_port       = "443"
}
resource "openstack_fw_rule_v2" "rule_4" {
  name              = "allow-udp-85.119.149.24-443"
  action            = "allow"
  protocol          = "tcp"
  source_ip_address = "85.119.149.24"
  source_port       = "443"
}
resource "openstack_fw_rule_v2" "rule_5" {
  name              = "allow-occm-443"
  action            = "allow"
  protocol          = "tcp"
  source_ip_address = "95.213.160.182"
  source_port       = "443"
}
resource "openstack_fw_rule_v2" "rule_6" {
  name                   = "allow-to-cloud-server"
  action                 = "allow"
  protocol               = "any"
  destination_ip_address = openstack_networking_floatingip_v2.floatingip_1.fixed_ip
}
resource "openstack_fw_policy_v2" "firewall_policy_2" {
  name    = "egress-firewall-policy"
  audited = true
  rules = [
    openstack_fw_rule_v2.rule_01.id,
    openstack_fw_rule_v2.rule_02.id,
    openstack_fw_rule_v2.rule_03.id,
    openstack_fw_rule_v2.rule_04.id,
    openstack_fw_rule_v2.rule_05.id,
  ]
}
resource "openstack_fw_rule_v2" "rule_01" {
  name                   = "allow-udp-188.93.16.19-53"
  action                 = "allow"
  protocol               = "udp"
  destination_ip_address = "188.93.16.19"
  destination_port       = "53"
}
resource "openstack_fw_rule_v2" "rule_02" {
  name                   = "allow-udp-188.93.17.19-53"
  action                 = "allow"
  protocol               = "udp"
  destination_ip_address = "188.93.17.19"
  destination_port       = "53"
}
resource "openstack_fw_rule_v2" "rule_03" {
  name                   = "allow-udp-92.53.68.16-443"
  action                 = "allow"
  protocol               = "tcp"
  destination_ip_address = "92.53.68.16"
  destination_port       = "443"
}
resource "openstack_fw_rule_v2" "rule_04" {
  name                   = "allow-udp-85.119.149.24-443"
  action                 = "allow"
  protocol               = "tcp"
  destination_ip_address = "85.119.149.24"
  destination_port       = "443"
}
resource "openstack_fw_rule_v2" "rule_05" {
  name                   = "occm-443"
  action                 = "allow"
  protocol               = "tcp"
  destination_ip_address = "95.213.160.182"
  destination_port       = "443"
}
resource "openstack_fw_group_v2" "group_1" {
  name                       = "my-firewall"
  admin_state_up             = true
  ingress_firewall_policy_id = openstack_fw_policy_v2.firewall_policy_1.id
  egress_firewall_policy_id  = openstack_fw_policy_v2.firewall_policy_2.id
  ports = [
    openstack_networking_router_interface_v2.router_interface_1.port_id,
  ]
}


Первый ресурс — firewall_policy. Это набор из шести правил, которые разрешают подключения к разным IP-адресам и портам. Например, первое правило разрешает UDP на 53-й порт, то есть на IP-адреса, которые мы отмечали как рекурсоры. А шестое — «разрешает все» для нашего облачного сервера, который мы используем как Jump Host. Все правила после создания кластера можно будет посмотреть в панели управления.

Второй ресурс — firewall_policy_2. Здесь пять правил, которые дублируют все, что мы видели ранее. Первое правило — это один из RDNS, далее идет второй RDNS, репозиторий, mirror и наш OpenStack API.

В конце создаем еще один ресурс, в котором объединяем все наши правила и политики Ingress и Egress в одну группу. Назовем ее my-firewall и укажем порт роутера, который мы создали в манифесте ранее. Пишем yes, видим 21 ресурс для создания, запускаем.

Манифест cloud-server

resource "selectel_vpc_keypair_v2" "keypair_1" {
  name       = "keypair"
  public_key = file("~/.ssh/id_rsa.pub")
  user_id    = selectel_iam_serviceuser_v1.serviceuser_1.id
  regions    = ["ru-9"]
}
#TODO id_rsa
resource "openstack_compute_flavor_v2" "flavor_1" {
  name      = "custom-flavor"
  vcpus     = 2
  ram       = 2048
  disk      = 0
  is_public = false
  lifecycle {
    create_before_destroy = true
  }
}
resource "openstack_networking_port_v2" "port_1" {
  name       = "cloud-server-port"
  network_id = openstack_networking_network_v2.network_1.id
  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.subnet_1.id
  }
}
data "openstack_images_image_v2" "image_1" {
  depends_on  = [selectel_vpc_project_v2.project_1]
  name        = "Ubuntu 22.04 LTS 64-bit"
  most_recent = true
  visibility  = "public"
}
resource "openstack_blockstorage_volume_v3" "volume_1" {
  name        = "cloud-server-boot-volume"
  size        = "5"
  image_id    = data.openstack_images_image_v2.image_1.id
  volume_type = "fast.ru-9a"
  lifecycle {
    ignore_changes = [image_id]
  }
}
#### Create server
resource "openstack_compute_instance_v2" "server_1" {
  name              = "cloud-server"
  flavor_id         = openstack_compute_flavor_v2.flavor_1.id
  key_pair          = selectel_vpc_keypair_v2.keypair_1.name
  availability_zone = "ru-9a"
  network {
    port = openstack_networking_port_v2.port_1.id
  }
  lifecycle {
    ignore_changes = [image_id]
  }
  block_device {
    uuid             = openstack_blockstorage_volume_v3.volume_1.id
    source_type      = "volume"
    destination_type = "volume"
    boot_index       = 0
  }
}
resource "openstack_networking_floatingip_v2" "floatingip_1" {
  pool = "external-network"
}
resource "openstack_networking_floatingip_associate_v2" "association_1" {
  port_id     = openstack_networking_port_v2.port_1.id
  floating_ip = openstack_networking_floatingip_v2.floatingip_1.address
}
output "public_ip_address" {
  value = openstack_networking_floatingip_v2.floatingip_1.address
}


Рассмотрим, что есть в этом манифесте.

  • keypair. Это ключ, который лежит локально на ноутбуке. Мы добавляем его в облако, чтобы пользоваться SSH-ключами, а не ходить на Jump Host по паролям.
  • flavor_1. Здесь описываем количество ресурсов, необходимых хосту.
  • port_1. Это сетевой порт для сервера, чтобы мы смогли подключить его к приватной сети и иметь доступ к ресурсам внутри нее.
  • Ubuntu 22.04 — образ ОС.
  • volume_1. Это загрузочный диск, который мы будем использовать для загрузки сервера.


Когда ресурсы описаны, создаем сервер, описываем созданный порт и block_device, добавляем floating-IP. Он нужен, чтобы попасть на Jump Host из публичной сети. Далее необходимо ассоциировать floating-IP с портом и вывести public_ip_address. К нему уже можно подключаться по SSH.

Манифест k8s-cluster

data "selectel_mks_kube_versions_v1" "versions" {
  project_id = selectel_vpc_project_v2.project_1.id
  region     = "ru-9"
}
resource "selectel_mks_cluster_v1" "cluster_1" {
  name                              = "terraform-k8s-cluster"
  project_id                        = selectel_vpc_project_v2.project_1.id
  region                            = "ru-9"
  kube_version                      = data.selectel_mks_kube_versions_v1.versions.latest_version
  network_id                        = openstack_networking_network_v2.network_1.id
  subnet_id                         = openstack_networking_subnet_v2.subnet_1.id
  maintenance_window_start          = "00:00:00"
  enable_patch_version_auto_upgrade = false
  private_kube_api                  = true
}
resource "selectel_mks_nodegroup_v1" "nodegroup_1" {
  depends_on        = [selectel_mks_cluster_v1.cluster_1]
  cluster_id        = selectel_mks_cluster_v1.cluster_1.id
  project_id        = selectel_mks_cluster_v1.cluster_1.project_id
  region            = selectel_mks_cluster_v1.cluster_1.region
  availability_zone = "ru-9a"
  nodes_count       = "1"
  cpus              = 2
  ram_mb            = 4096
  volume_gb         = 32
  volume_type       = "fast.ru-9a"
}
data "selectel_mks_kubeconfig_v1" "kubeconfig" {
  cluster_id = selectel_mks_cluster_v1.cluster_1.id
  project_id = selectel_mks_cluster_v1.cluster_1.project_id
  region     = selectel_mks_cluster_v1.cluster_1.region
}
output "kubeconfig" {
  value = data.selectel_mks_kubeconfig_v1.kubeconfig.raw_config
  sensitive = true
}


Здесь мы начинаем с того, что определяем Versions. Это нужно для управления переменной kube_versions. А уже с ее помощью мы можем создавать кластеры из доступных версий и обновлять этот кластер.

Переменная, ради которой мы здесь собрались, — private_kube_api. Для нее указываем значение true. Она заставит Managed Kubernetes создать кластер с приватным Kube API.

В nodegroup_1 указываем необходимые ресурсы и определяем, что диск — это быстрый диск в пуле ru-9a и одна нода. Далее создаем kubeconfig, чтобы мы могли не ходить в панель и скачивать ее прямо из консоли. В конце выводим kubeconfig в output. В конце пишем yes. Когда кластер будет создан, увидим в терминале ответ:»Apply Complete! Ressources: 2 added, 1 changed, 0 destroyed».

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


Теперь в панели управления можно увидеть кластер, который мы создали только что. Здесь есть группа нод, в ней пока один сервер.

9afeaacc323bc8d7d47b60786df0fb6d.png


Если нужно изменить количество групп нод, возвращаемся в манифест k8s-cluster, копируем ресурс nodegroup, задаем ему новое имя и указываем нужные параметры. Допустим, изменим количество нод и CPU:

resource "selectel_mks_nodegroup_v1" "nodegroup_2" {
  depends_on        = [selectel_mks_cluster_v1.cluster_1]
  cluster_id        = selectel_mks_cluster_v1.cluster_1.id
  project_id        = selectel_mks_cluster_v1.cluster_1.project_id
  region            = selectel_mks_cluster_v1.cluster_1.region
  availability_zone = "ru-9a"
  nodes_count       = "2"
  cpus              = 4
  ram_mb            = 4096
  volume_gb         = 32
  volume_type       = "fast.ru-9a"
}


Когда все готово, снова выполняем Terraform Apply.

Тем временем в панели управления можно посмотреть настройки кластера, Kube API, ранее созданной подсети, его тип и количество мастер-нод. Видим, что у нас их три — если одна выйдет из строя, кластер продолжит работать.

791ee89232590a94b202f31b26b8cbc4.png


Настройка в консоли


Сейчас нам необходим файл kubectl. Обычно скачать его можно командой:

-curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release.stable.txt)/bin/linux/amd64/kubectl"


В нашем случае ничего не произойдет, потому что мы ранее настроили облачный файрвол, который блокирует все эти запросы.

Стоит заранее скачать файл и положить в соседнюю директорию. Так будет удобнее его доставать.

./artifact/kubectl @root46.148.228.149:./


Когда убедились, что файл kubectl скачан, выполняемchmod +x. Проверяем работоспособность нашего kubectl командой:

-curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release.stable.txt)/bin/linux/amd64/kubectl"


Для доступа в кластер необходим еще kubeconfig. В консоли вводим команду:

terraform output kubeconfig


После получения файла его можно переименовать, например, в kubeconf.conf:

terraform output kubeconfig > kubeconf.conf


Далее, чтобы скопировать сам kubeconfig, вводим команду:

scp ./kubeconf.conf root@46.148.228.149:./


Следующий шаг выполняем на сервере — экспортируем kubeconfig и выводим его:

export KUBECONFIG=~/kubeconf.conf
./kubectl get no


Далее видим вывод kubectl get node: все ноды и время их создания. Давайте посмотрим на системные поды:

./kubectl -n -kube-system get po


Если все сделано правильно, увидим, что все поды запущены, то есть находятся в состоянииrunning. Это говорит о том, что настройки файрвола помогли нам достучаться до необходимых компонентов.

Заключение


Мы развернули приватную инсталляцию в публичном облаке, прикрыли ее межсетевым экраном и можем спокойно работать на гибридной инфраструктуре. Вы можете это повторить с помощью Managed Kubernetes, облачного сервера и облачного файрвола. Если у вас остались вопросы, задавайте их в комментариях. И не забывайте там же делиться своим опытом.

Если вам удобнее ознакомиться с материалом в формате видео, то предлагаем посмотреть запись воркшопа:

© Habrahabr.ru