Приватный Kubernetes за 50 минут
Вынести даже часть инфраструктуры из закрытого контура — это ответственный и сложный шаг для многих компаний. Этот момент особенно чувствителен для сфер промышленности, финансов и государственного сектора.
Привет, Хабр! Меня зовут Саша, я ведущий системный администратор в Selectel. В этой статье на примере гибридной инфраструктуры покажу, как развернуть защищенную приватную инсталляцию в облаке, а заодно разберу некоторые мифы. Добро пожаловать под кат!
Используйте навигацию, если не хотите читать текст полностью:
→ Приватные инсталляции Managed Kubernetes
→ Bare Metal Cloud
→ Построение гибридной архитектуры
→ Заключение
Приватные инсталляции Managed Kubernetes
Приватные инсталляции существуют в трех вариантах. Есть приватные хосты, приватные сегменты пула и приватные пулы. Разберем подробнее каждый из них.
Приватный хост
Это изоляция вычислений на уровне физического хоста. Запуск виртуальной машины или воркер-ноды (worker node) Kubernetes происходит на физическом сервере. Он становится приватным хостом, если целиком отдается одному пользователю.
В таком варианте нет конкуренции за вычислительные ресурсы или вероятности, что кто-то скомпрометирует ваши данные. Они изолируются на конкретном хосте, доступ к которому есть только у одного пользователя или компании.
В облаке Selectel можно создать облачный сервер или воркер-ноду кластера Kubernetes с поддержкой защищенных анклавов. Это технология, которая предоставляет набор инструкций центрального процессора для повышения безопасности кода и данных приложения с помощью дополнительной защиты от утечек или модификации.
Упрощенно это выглядит так: защищаемое приложение может создавать в оперативной памяти анклавы, данные в которых изолируются на физическом хосте. Их не могут прочитать другие приложения, запущенные на том же сервере (включая системные, модули ядра и гипервизор).
Приватный сегмент пула
Это группа хостов, к которой можно подключить выделенное сетевое хранилище и хранилище бэкапов. Если для приватного хоста сетевые диски и бэкапы берутся из общего массива, то для приватного сегмента пула они уже включены в конфигурацию. Таким образом, приложение изолируется и на уровне хранения данных. Подробнее о приватных сегментах пула можно прочитать в документации Selectel.
Приватный пул
Это выделенные API-ноды, в которых можно изолироваться не только на уровне хостов, вычислений и хранения, но и на уровне сетевой связности, доступа и управляющих API-нод. Этот вариант изоляции хостов максимально близок к уровню изоляции частного облака.
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
». Теперь идем в панель управления. Проект уже здесь, но пока он пустой. Создадим новый кластер, а заодно посмотрим, какие у нас еще манифесты припасены.
Панель управления с пустым проектом 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
».
Просмотр кластера в панели управления
Теперь в панели управления можно увидеть кластер, который мы создали только что. Здесь есть группа нод, в ней пока один сервер.
Если нужно изменить количество групп нод, возвращаемся в манифест 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, ранее созданной подсети, его тип и количество мастер-нод. Видим, что у нас их три — если одна выйдет из строя, кластер продолжит работать.
Настройка в консоли
Сейчас нам необходим файл 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, облачного сервера и облачного файрвола. Если у вас остались вопросы, задавайте их в комментариях. И не забывайте там же делиться своим опытом.
Если вам удобнее ознакомиться с материалом в формате видео, то предлагаем посмотреть запись воркшопа: