Поднимаем инфру в SberCloud с Terraform

Поднимаем инфру в SberCloud с Terraform

You’re going to need a bigger boat.

Волею судеб в 2022м году среднему российскому сисадмину пришлось прикинуть всякое к носу и решить, как жить без таких привычных AWS, GCP и, прости хосспади, Azure. Выяснилось, что вопреки верещанию некоторых "интеллектуалов", клауды в РФ таки есть и более того, можно походить по рынку и выбрать. В качестве примера такого клауда возьмём SberCloud, а точнее тот его кусок, который зовётся advanced и попробуем натравить на него Terraform.

О чём пойдёт речь

Сразу оговорюсь - это не обзор функционала и не сравнение с другими игроками, хоть и жаль немного, что пропадает столько хороших шуток хотя бы на тему диффок доки того же Elastic Cloud Server в SberCloud и HuaweiCloud. Будем практичны - есть задача X а вот для неё решение Y (нам же ехать, а не шашечки).

Задача - поднять минимальный энв с managed Kubernetes и не сильно задолбаться. Под минимальным энвом будем понимать вот такую историю: k8s-sc

Решение - читаем дальше.

Terraform, Sbercloud - погнали

Когда знакомишься с очередным клауд-провайдером, после первых 20и минут, проведённых в веб-консоли у эникея здорового человека возникает закономерный вопрос - а как бы устроить IaC по заветам дяди Боба? Видно, что парни из сберклауда тоже озаботились этим вопросом - поиск по гитхабу даёт нам репозиторий с соответствующим терраформ провайдером. Беглый взгляд говорит, что репа вполне себе живая, есть собранные релизные бинари, какой-то движ в ишуях и пуллреквестах, чейнджлог - в общем, всё по-взрослому. Лично мне не терпелось, поэтому первым делом я пошёл в examples. А там... мама дорогая, практически всё, что может пригодиться для старта - там есть. Но дьявол, как всегда, в деталях.

Начинаем с конфигурации аутентификации терраформ клиента, делаем по инструкции отсюда. Но не всё так просто, в случае, когда у нас есть креды только для IAM юзера (а именно так было в моём случае), то нужно посетать ещё пару переменных окружения, а именно:

    $ export SBC_ENTERPRISE_PROJECT_ID="YOUR_ENTERPRISE_UUID"
    $ export SBC_PROJECT_NAME="YOUR_ENTERPRISE_PROJECT_NAME"

При этом, в SBC_PROJECT_NAME нужно писать имя не в том виде, в котором его видно в веб-консоли. Формат должен быть следующий: ${SBC_REGION_NAME}_${PROJECT_NAME_FROM_WEBCONSOLE}. Например ru-moscow-1_my-awesome-project. Если кто-то знает, где эта инфа лежит в документации - готов угостить пивом, потому что я это доставал из исходников провайдера.

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

Fuck you, Hashicorp

Нельзя не помянуть добрым словом менеджмент Hashicorp'a, ударившийся в модную в этом сезоне cancel culture. Для админов из РФ это означает следующее: вот ты набросал HCL кодец, собираешься приступить к отладке, пишешь в терминале terraform init и получаешь в руки известно что:

    
     Error: Failed to query available provider packages
     
     Could not retrieve the list of available versions for provider hashicorp/aws: could not connect to registry.terraform.io:
     Failed to request discovery document: 403 Forbidden
    

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

Вторая - это завести свой собственный registry сервер, который будет отдавать бинари провайдеров. Есть открытая (пока ещё) спецификация Provider Registry Protocol, никакого rocket science, при желании, минимальная рабочая версия реализуется за пару выходных (но это будет темой отдельного поста). Из готовых же opensource реализаций гуглится только terralist.

Обезьяна нашла третий путь

Третья лазейка - это заставить терраформ использовать в качестве registry какую-нибудь локальную директорию, в которую подложить заранее собранные\скачанные бинари провайдеров. В моём случае мне понадобится два провайдера:

  1. Sbercloud v1.10.0
  2. Local v.2.2.2

Сначала подготовим terraform:

Вот теперь terraform init корректно сделает всё, что нужно и можно будет двигаться дальше.

Даёшь Kubernetes!

Все переменные, использующиеся в HCL коде, определим потом отдельно.

Начнём с создания VPC, подсетей и секурити рулов, потом бастион и, наконец, сам кластер k8s.

resource "sbercloud_vpc" "vpc" {                                                                                              
  name = var.vpc_name
  cidr = var.vpc_cidr
  tags = var.tags
}

resource "sbercloud_vpc_subnet" "subnet" {
  name       = var.subnet_name
  cidr       = var.subnet_cidr
  gateway_ip = var.subnet_gateway_ip

  primary_dns   = var.subnet_dns1
  secondary_dns = var.subnet_dns2

  vpc_id = sbercloud_vpc.vpc.id

  tags = var.tags
}
locals {
  default_rules = {
    http-rule = {
      description = "Allow HTTP from anywhere",
      protocol = "tcp",
      port = 80,
      source = "0.0.0.0/0"
    },
    https-rule = {
      description = "Allow HTTPS from anywhere",
      protocol = "tcp",
      port = 443,
      source = "0.0.0.0/0"
    },
    ssh-rule = {
      description = "Allow SSH from anywhere",
      protocol = "tcp",
      port = 22,
      source = "0.0.0.0/0"
    }
  }
}
resource "sbercloud_networking_secgroup" "sg_default" {
  name        = var.default_security_group_name
  description = "Default security group"
}
resource "sbercloud_networking_secgroup_rule" "default_ingress_icmp" {
  direction         = "ingress"
  ethertype         = "IPv4"
  description       = "Allow ICMP from anywhere"
  protocol          = "icmp"
  remote_ip_prefix  = "0.0.0.0/0"

  security_group_id = sbercloud_networking_secgroup.sg_default.id
}
resource "sbercloud_networking_secgroup_rule" "default_ingress" {
  for_each = local.default_rules

  direction         = "ingress"
  ethertype         = "IPv4"
  description       = each.value.description
  protocol          = each.value.protocol
  port_range_min    = each.value.port
  port_range_max    = each.value.port
  remote_ip_prefix  = each.value.source

  security_group_id = sbercloud_networking_secgroup.sg_default.id
}
resource "sbercloud_compute_instance" "bastion" {
  name              = var.hostname
  image_id          = var.ecs_image_id
  flavor_id         = var.ecs_flavor
  security_groups   = [var.default_security_group_name]
  availability_zone = var.vpc_az
  key_pair          = var.keypair_name
  system_disk_type  = var.ecs_system_disk_type
  system_disk_size  = var.ecs_system_disk_size
  # как и в любом уважающем себя клауде, инстансы в SberCloud умеют в cloud-init
  user_data         = file("${path.module}/setup.sh")
  tags              = var.tags
  network {
    uuid = var.subnet_id
  }
}
resource "sbercloud_vpc_eip" "bastion_eip" {
  tags = var.tags
  publicip {
    type = "5_bgp"
  }
  bandwidth {
    name        = "elb-bastion-bandwidth"
    size        = var.bandwidth_size
    share_type  = "PER"
    charge_mode = var.bandwidth_charge_mode
  }
}
resource "sbercloud_compute_eip_associate" "associated_01" {
  public_ip   = sbercloud_vpc_eip.bastion_eip.address
  instance_id = sbercloud_compute_instance.bastion.id
}
resource "sbercloud_cce_cluster" "default" {
  name                   = var.cce_cluster_name
  flavor_id              = var.cce_flavor
  container_network_type = "overlay_l2"
  container_network_cidr = var.subnet_cidr
  multi_az               = false
  vpc_id                 = var.vpc_id
  subnet_id              = var.subnet_id
}
resource "sbercloud_cce_node_pool" "default" {
  cluster_id               = sbercloud_cce_cluster.default.id
  name                     = var.cce_nodepool_name
  flavor_id                = var.cce_node_flavor
  availability_zone        = var.vpc_az
  key_pair                 = var.keypair_name
  scall_enable             = true
  min_node_count           = 1
  initial_node_count       = 1
  max_node_count           = var.max_node_count
  scale_down_cooldown_time = 100
  priority                 = 1
  type                     = "vm"
  os                       = "CentOS 7.6"
  tags                     = var.tags

  root_volume {
    size       = 50
    volumetype = "SAS"
  }

  data_volumes {
    size       = 100
    volumetype = "SAS"
  }
}
resource "local_file" "kubeconfig" {
  depends_on   = [sbercloud_cce_cluster.default]
  filename     = "./kubeconfig"
  # А вот тут нам и пригодился провайдер local - он нам сгенерит конфиг для kubectl
  content      = sbercloud_cce_cluster.default.kube_config_raw
}

Итого

На данном этапе мы подготовили примерный код, который можно аккуратно разложить по директориями и запустить наконец terraform apply, после чего можно заходить по ssh на бастион и запускать всякое в кластере k8s (не забыв забрать сгенерённый kubeconfig). Готовый к использованию код можно найти тут. Подробнее про варианты конфигурации ресурсов в SberCloud можно почитать в документации.