Полагаю, каждый разработчик рано или поздно приходит к мысли о том, что ему есть, что рассказать и чем поделиться. Кто-то даже начинает это делать в том или ином формате. И, конечно, хочется сказать спасибо всем тем, кто отвечает на вопросы на stackoverflow, пишет статьи или делает еще какой-либо контент. Однако быть автором труд весьма специфичный, всегда есть риск, что твой контент не будет полезен или даже интересен. За несколько лет мною было написано около пары десятков статей, а также было начато несколько своих проектов, но все это выглядит на первый взгляд как «работа в стол».

Однако, порефлексировав, я понял, что вся работа, которая не дает ожидаемого результата не напрасна. Как минимум - это опыт, а опыт штука полезная. Никогда не угадаешь, что тебе может пригодится в будущем. К тому же не стоит сбрасывать со счетов накопительный эффект общей массы знаний. Звучит довольно туманно, поэтому приведу пример. Разработка показала мне, как можно копить фрагментированные кусочки знаний долгое время, которые в определенный момент могут сложится в полную картину, а ты будешь удивляться, как не понимал чего-то раньше.

Так о чем это я? Я буду делать личный блог или сайт, на самом деле еще не знаю во что это выльется. Но как показал опрос в моем TG канале, у ребят, как и у меня, есть интерес к тому, как можно сделать и использовать блог, чтобы он приносил тебе пользу в каком-либо виде. Если дело пойдет, то здесь будет целая арка статей. Приступим!

С чего начать?

На самом деле в голосовании в своем канале, я обозначил более широкую тему: «Как сделать личный сайт, чтобы он в каком-то виде работал на тебя?». То есть речь шла о чем угодно, от блога до сайта студии по разработке. Но сделать что-то с таким широким выбором трудно, поэтому пообщавшись с ребятами, я решил начать с малого - личный блог.

Получается первое, что нужно сделать - определиться с форматом (блог, сайт студии и т.п.) и придерживаться его.

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

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

То есть второй пункт - выбор инструментов.

Конечно сразу же приходит на ум такой инструмент, как Astro. На если использовать Astro и делать обычный статический блог, то с большой долей вероятности, здесь бы была только одна статья, а у меня бы появился блог, в который мне бы пришлось как-то привлекать трафик. И скорее всего - все это также бы пошло в стол. Поэтому я выбрал путь поинтереснее. А именно самостоятельно развернуть инфраструктуру, используя IaC подход, и написать динамический блог с cms на минималках, используя SSR.

Таким образом, я попробую «убить нескольких зайцев одним камнем». А именно:

  1. Попрактиковаться в конфигурации облачной инфраструктуры.

  2. Попробовать наконец-то SSR, а то для внутрекорпоративных продуктов он не в ходу.

  3. Сделать блог.

Далее идет третий пункт - декомпозиция. На данном этапе я разделил задачу на три шага:

  1. Написание конфигурации облачной инфраструктуры с помощью Terraform в облаке Selectel.

  2. Настройка развернутой инфраструктуры с помощью Ansible.

  3. Написание блога (Декомпозицию этого пункта проведу позже).

Немного о Terraform

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

Terraform появился на свет благодаря HashiCorp и стал довольно популярным. Он хранит свой конфиг в файлах, написанных на HCL (ямлоподобные конфиги). При изменении файлов конфига Terraform автоматически определяет, что уже развернуто, а что следует добавить или удалить.

Terraform можно использовать для работы с любым REST API, правда, для API нужен особый плагин под названием провайдер. Полный список провайдеров для любых облачных сервисов можно найти в Terraform Registry. Кроме того, из Terraform Registry можно загрузить и готовые рецепты тех или иных сервисов — модули.

Обычно все облачные сервисы пишут подробные инструкции для работы с их API через Terraform. Вот некоторые из них:

  1. Selectel

  2. Yandex Cloud

  3. Google Cloud

К сожалению, сейчас, находясь в России, нельзя просто так взять и скачать себе Terraform. Вам потребуется VPN или зеркало. Если вы использовали зеркало, то определите в переменной PATH путь к бинарнику:

export PATH=$PATH:<path>

Установите Terraform, используя инструкцию из документации HashiCorp или эту инструкцию.

Теперь о работе с Terraform. Если кратко, то алгоритм работы следующий:

  1. Определить, из каких элементов состоит инфраструктура.

  2. Написать конфигурационные файлы.

  3. terraform init — установить плагины Terraform, необходимые для используемых элементов.

  4. terraform plan — посмотреть, какие конкретно изменения Terraform внесёт в существующую инфраструктуру.

  5. terraform apply — применить эти изменения.

Из чего состоит конфигурация? 

Провайдер в Terraform — это плагин для работы с каким-либо сервисом. Для популярных сервисов (облачных и не очень) уже написаны плагины-провайдеры. Если для управления инфраструктурой при помощи Terraform этих провайдеров недостаточно, можно написать свой плагин.

Пример конфигурации провайдеры для Selectel:

# Initialize Selectel provider with service user.
provider "selectel" {
  username    = var.username
  password    = var.password
  domain_name = var.domain_name
  auth_region = var.region
  auth_url    = var.auth_url
}

Ресурс — это описание сущности, которую можно создать в API сервиса (с ним общается провайдер). Список доступных ресурсов можно подсмотреть в документации провайдера, я буду использовать провайдеры Selectel и Openstack.

Пример конфигурации хранилища от Selectel:

resource "openstack_blockstorage_volume_v3" "volume_1" {
  name              = "volume-for-${var.server_name}"
  size              = var.server_root_disk_gb
  image_id          = module.image_datasource.image_id
  volume_type       = var.server_volume_type
  availability_zone = var.server_zone
  metadata          = var.server_volume_metadata

  lifecycle {
    ignore_changes  = [image_id]
  }
}

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

Пример определения datasource и его применения в ресурсе:

data "openstack_networking_network_v2" "external_net" {
  name     = var.router_external_net_name
  external = true
}

resource "openstack_networking_router_v2" "router_1" {
  name                = var.router_name
  external_network_id = data.openstack_networking_network_v2.external_net.id
}

Variables - переменные в Terraform можно поделить на три вида:

  1. Input переменные.

  2. Output переменные.

  3. Local переменные.

Если провести аналогию с языком программирования, то Input переменные — аргументы функции. Каждая входная переменная, принимаемая модулем, должна быть объявлена c помощью блока variable:

variable "server_zone" {
  default         = "ru-9a"
  type            = string
  description     = "Instance availability zone"
  validation {
    condition     = contains(toset(["ru-1a", "ru-3a", "ru-9a"]), var.instance_zone)
    error_message = "Select availability zone from the list: ru-1a, ru-3a, ru-9a."
  }
  sensitive       = true
  nullable        = false
}

Output переменные похожи на возвращаемые значения в языках программирования. Они нужны, чтобы показать информацию о вашей инфраструктуре в командной строке, а также поделиться информацией для использования другими конфигурациями Terraform. Такие переменные можно определять в output.tf и использовать дальше в конфигурации.

output "server_id" {
  value = openstack_compute_instance_v2.instance_1.id
}

Local переменные напоминают локальные переменные в функциях. Они задаются с помощью блока locals:

locals {
  service_name = "virtual machine"
  owner        = «Selectel
}

Локальные переменные полезны, когда в конфигурации есть многократное повторение одних и тех же значений.

Задавать значения переменных можно, используя дефолтные значения при их объявлении, как показано в примерах выше, а также можно использовать следующие методы:

  1. В cli через -var (поштучно):

$ terraform apply -var="server_zone=ru-9a"
  1. Через файл с расширением .tfvars (пачками). Создаём файл *.tfvars:

server_zone=ru-9a

По умолчанию загружаются значения из terraform.tfvars, но можно явно обозначить файл для загрузки:

$ terraform apply -var-file="testing.tfvars"
  1. Через переменные окружения. Переменная должна начинаться с TF_VAR_, а дальше уже имя переменной:

# для простого типа
$ export TF_VAR_server_zone=ru-9a

# для составного типа
$ export TF_VAR_server_zone='["ru-9a","ru-3a"]'

$ terraform apply

Подробнее о переменных можно почитать здесь.

Модуль — набор конфигурационных файлов в одной директории. Если в каталоге лежит хотя бы один файл конфигурации Terraform .tf — это уже модуль. Структура модуля обычно выглядит так:

├── LICENSE
├── README.md
├── main.tf
├── modules/
├── variables.tf
├── outputs.tf

Подробнее про структуру можно почитать здесь.

Модули бывают корневые (root) и дочерние (child). Корневой — наш модуль, а дочерними будут все модули, которые мы подключим в корневом. Дочерние модули можно хранить локально, положить в папку modules в рутовом модуле, или брать с удалённых registry (GitLab, HTTP URL, Terraform Registry).

Мета-аргументы - Язык Terraform определяет несколько мета-аргументов, которые можно использовать с любым типом ресурсов для изменения их поведения. К ним относятся:

  1. depends_on — для указания скрытых зависимостей.

  2. count — для создания нескольких экземпляров ресурсов, количество экземпляров = count. Подробности тут.

  3. for_each создаёт несколько экземпляров модуля из одного блока модуля. За подробностями сюда.

  4. provider — для выбора конфигурации провайдера. Дополнительно читать тут.

  5. lifecycle — для настройки жизненного цикла. Сейчас не используется, но зарезервирован на будущее.

За подробностями по мета-аргументам стоит заглянуть сюда.

Приступаю к настройке

Прежде чем я начну описывать инфраструктуру, должен отметить, что основная моя специализация - это Frontend разработка, а мой уровень знаний по DevOps складывается из решения различных вопросов на работе и курса от Яндекс.Практикум, что я прошел год назад. Поэтому сразу скажу, что мои решения не претендуют на прилагательное «идеальные», скорее всего я могу чего-то не знать и рассчитываю на вашу помощь, если вы имеете компетенцию в этой сфере.

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

Когда я только приступил к этой задаче, я пробовал самостоятельно собрать конфигурацию используя инструкцию и реестр ресурсов от Selectel, но быстро понял, что это наверняка делали до меня и есть какие-либо готовые модули. Собственно так и есть. Я нашел статью от Selectel, где я нашел репозиторий с готовыми модулями. Из которых я собрал следующее:

├── README.md
├── main.tf
├── modules/
├──── flavor/
├──── floatingip/
├──── image_datasource/
├──── keypair/
├──── nat/
├──── project/
├──── project_with_user/
├──── server_remote_root_disk/
├── vars.tf
├── versions.tf
├── outputs.tf

Для начала отдельно отмечу файл versions.tf. Там указаны версии провайдеров, которые будут использоваться при инициализации Terraform:

terraform {
  required_providers {
    selectel = {
      source  = "selectel/selectel"
      version = ">= 5.1.1"
    }
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = ">= 2.0.0"
    }
  }
  required_version = ">= 1.9.2"
}

Теперь стоит рассказать о корневом модуле, для этого рассмотрим файл main.tf:

# Initialize Selectel provider with service user.
provider "selectel" {
  username    = var.username
  password    = var.password
  domain_name = var.domain_name
  auth_region = var.region
  auth_url    = var.auth_url
}

# Create the main project with user.
# This module should be applied first:
# terraform apply -target=module.project_with_user
module "project_with_user" {
  source = "./modules/project_with_user"

  project_name      = var.project_name
  project_user_name = var.project_user_name
  user_password     = var.user_password
}

# Initialize Openstack provider.
provider "openstack" {
  user_name           = var.project_user_name
  tenant_name         = var.project_name
  password            = var.user_password
  project_domain_name = var.domain_name
  user_domain_name    = var.domain_name
  auth_url            = var.auth_url
  region              = var.region
}

# Create an OpenStack Compute instance.
module "server" {
  source = "./modules/server_remote_root_disk"

  # OpenStack Instance parameters.
  keypair_name           = var.keypair_name
  server_group_id        = var.server_group_id
  server_image_name      = var.server_image_name
  server_license_type    = var.server_license_type
  server_name            = var.server_name
  server_preemptible_tag = var.server_preemptible_tag
  server_ram_mb          = var.server_ram_mb
  server_root_disk_gb    = var.server_root_disk_gb
  server_vcpus           = var.server_vcpus
  server_volume_metadata = var.server_volume_metadata
  server_volume_type     = var.server_volume_type
  server_zone            = var.server_zone

  depends_on = [
    module.project_with_user,
  ]
}

Здесь определяются два провайдера: Selectel и Openstack, также два модуля: project_with_user и server. Большинство переменных задано в файле vars.tf с помощью дефолтных значений, за исключением следующих четырех:

  • username - имя сервисного пользователя, которого вы должны создать в админке Selectel, подробнее здесь.

  • password - пароль от вышеупомянутого сервисного пользователя.

  • domain_name - идентификатор аккаунта Selectel, который можно посмотреть в панели управления в правом верхнем углу.

  • user_password - любой придуманный вами пароль для нового сервисного пользователя, который будет управлять созданным проектом.

Как вы могли заметить модуль server зависит от project_with_user, поэтому применять конфигурацию необходимо с помощью двух команд apply:

env \
  TF_VAR_username=USER \
  TF_VAR_password=PASSWORD \
  TF_VAR_domain_name=ACCOUNT_ID \
  TF_VAR_user_password=xxx \
  terraform apply -target=module.project_with_user

env \
  TF_VAR_username=USER \
  TF_VAR_password=PASSWORD \
  TF_VAR_domain_name=ACCOUNT_ID \
  TF_VAR_user_password=xxx \
  terraform apply

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

flavor

Flavor - это предустановленная конфигурация, определяющая вычислительные ресурсы, память и емкость хранилища экземпляра ВМ. В проекте я его оставил для примера, а при создании ВМ использую идентфикатор готового Flavor от Selectel, все варианты которых можно посмотреть здесь. Код ресурса:

resource "openstack_compute_flavor_v2" "flavor_1" {
  name      = var.flavor_name
  ram       = var.flavor_ram_mb
  vcpus     = var.flavor_vcpus
  disk      = var.flavor_local_disk_gb
  is_public = var.flavor_is_public

  lifecycle {
    create_before_destroy = true
  }
}

floatingip

Плавающий IP-адрес — это своего рода виртуальный IP-адрес, который можно динамически перенаправлять на любой сервер в той же сети. Он используется для подключения к ВМ, как публичный IP. Код ресурса:

resource "openstack_networking_floatingip_v2" "floatingip_1" {
  pool = "external-network"
}

image_datasource

Источник данных, где хранится информация об образе системы, которую нужно будет развернуть на ВМ. Код:

data "openstack_images_image_v2" "image_1" {
  name        = var.image_name
  visibility  = "public"
  most_recent = true
}

keypair

Модуль, через который можно подвязать ваш публичный ssh-ключ к созданному сервизному пользователю:

module "keypair" {
  count  = var.keypair_name != "" ? 1 : 0
  source = "../keypair"

  keypair_name       = var.keypair_name
  keypair_public_key = file("~/.ssh/id_rsa.pub")
  keypair_user_id    = selectel_iam_serviceuser_v1.serviceuser_1.id
}

nat

Модуль, который создает объекты NAT. Network address translation (NAT) - это метод сопоставления одного пространства IP-адресов с другим путем изменения информации о сетевых адресах в IP-заголовке пакетов. Код модуля:

data "openstack_networking_network_v2" "external_net" {
  name     = var.router_external_net_name
  external = true
}

resource "openstack_networking_router_v2" "router_1" {
  name                = var.router_name
  external_network_id = data.openstack_networking_network_v2.external_net.id
}

resource "openstack_networking_network_v2" "network_1" {
  name = var.network_name
}

resource "openstack_networking_subnet_v2" "subnet_1" {
  network_id      = openstack_networking_network_v2.network_1.id
  dns_nameservers = var.dns_nameservers
  name            = var.subnet_cidr
  cidr            = var.subnet_cidr
}

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
}

project

Модуль для создания проекта в Selectel. Код:

resource "selectel_vpc_project_v2" "project_1" {
  name        = var.project_name
}

project_with_user

Модуль, который использует модуль project и создает сервисного пользователя для этого проекта с привязанным ssh-ключом. Код модуля:

module "project" {
  source       = "../project"
  project_name = var.project_name
}
resource "selectel_iam_serviceuser_v1" "serviceuser_1" {
  name     = var.project_user_name
  password = var.user_password
  role {
    role_name = "member"
    scope     = "project"
    project_id = module.project.project_id
  }
}

module "keypair" {
  count  = var.keypair_name != "" ? 1 : 0
  source = "../keypair"

  keypair_name       = var.keypair_name
  keypair_public_key = file("~/.ssh/id_rsa.pub")
  keypair_user_id    = selectel_iam_serviceuser_v1.serviceuser_1.id
}

server_remote_root_disk

Модуль для создания виртуальной машины с внешним хранилищем, который использует созданные объекты NAT и публичный IP. Код модуля:

resource "random_string" "random_name" {
  length  = 5
  special = false
}

# module "flavor" {
#   source        = "../flavor"
#   flavor_name   = "flavor-${random_string.random_name.result}"
#   flavor_vcpus  = var.server_vcpus
#   flavor_ram_mb = var.server_ram_mb
# }

module "nat" {
  source = "../nat"
}

resource "openstack_networking_port_v2" "port_1" {
  name       = "${var.server_name}-eth0"
  network_id = module.nat.network_id

  fixed_ip {
    subnet_id = module.nat.subnet_id
  }
}

module "image_datasource" {
  source     = "../image_datasource"
  image_name = var.server_image_name
}

resource "openstack_blockstorage_volume_v3" "volume_1" {
  name              = "volume-for-${var.server_name}"
  size              = var.server_root_disk_gb
  image_id          = module.image_datasource.image_id
  volume_type       = var.server_volume_type
  availability_zone = var.server_zone
  metadata          = var.server_volume_metadata

  lifecycle {
    ignore_changes = [image_id]
  }
}

resource "openstack_compute_instance_v2" "instance_1" {
  name              = var.server_name
  # flavor_id          = module.flavor.flavor_id
  // NOTE: [Denis Voronin] 25.12.2024 - flavor_id is shared Line fixed configurations with vCPU share of 10%;
  // https://docs.selectel.ru/en/cloud/servers/create/configurations/#shared-line
  flavor_id          = "9011"
  key_pair          = var.keypair_name
  availability_zone = var.server_zone

  network {
    port = openstack_networking_port_v2.port_1.id
  }

  dynamic "network" {
    for_each = var.server_license_type != "" ? [var.server_license_type] : []

    content {
      name = var.server_license_type
    }
  }

  block_device {
    uuid             = openstack_blockstorage_volume_v3.volume_1.id
    source_type      = "volume"
    destination_type = "volume"
    boot_index       = 0
  }

  tags = var.server_preemptible_tag

  vendor_options {
    ignore_resize_confirmation = true
  }

  dynamic "scheduler_hints" {
    for_each = var.server_group_id != "" ? [var.server_group_id] : []
    content {
      group = var.server_group_id
    }
  }
}

module "floatingip" {
  source = "../floatingip"
}

resource "openstack_networking_floatingip_associate_v2" "association_1" {
  port_id     = openstack_networking_port_v2.port_1.id
  floating_ip = module.floatingip.floatingip_address
}

Если применить конфигурацию, то по результату мы получим публичный IP адрес, который можно использовать для подключения к ВМ с помощью SSH:

ssh remote_username@remote_host

remote_username в нашем случае будет root, а remote_host - наш публичный IP.

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

Также хотелось бы рассказать про стоимость такой конфигурации. Я оставил машину на сутки и вот что у меня вышло при отсутствии нагрузки:

22 рубля с копейками в сутки или почти 670 рублей в месяц. Для статического блога было бы дорогавто)

На этом все. Спасибо всем, кто дочитал до конца!) Буду рад коммаентариям и если зайдете в мой TG канал.

Комментарии (7)


  1. ExternalWayfarer
    27.12.2024 12:54

    Буду рад коммаентариям и если зайдете в мой TG канал.

    Не зайдём :)


  1. MAXH0
    27.12.2024 12:54

    Всегда делай то, что собрался делать! Если собрался завести блог, надо ЗАВЕСТИ БЛОГ. Не программировать его. Не настраивать серверные части. А именно - завести блог. Простейшим способом. Вордпресс, генератор статики, любой сервис - без разницы. Садишься и пишешь свой Веб лог...

    Сам всегда делал эту ошибку. Она стоила мне 10 лет пробуксовки когда я шлифовал кирпич пытаясь довести до совершенства то, чем в итоге и не стал пользоваться.


    1. Goodzonchik
      27.12.2024 12:54

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

      Для блогов есть уже много готовых ресурсов, особенно для начинающего блогера завести ТГ, ВК - вполне неплохой вариант, чем сделать сайт, на который нужно еще как-то нагнать трафик.

      Для остального есть докер и гитхаб)


  1. dsoastro
    27.12.2024 12:54

    Какой смысл для настройки сервака для блога использовать терраформ и потом ансибл? Если это условно однократное событие руками проще на порядок сделать. Ансибл хоть как-то можно понять - при переезде к другому провайдеру плейбук менять не придется, но код терраформа специфичен для провайдера. Стрельба из пушек по воробьям


  1. MrAlone
    27.12.2024 12:54

    Когда девелопер вдруг решает, что он может в инфраструктуру, получается вот такое.


  1. gen1lee
    27.12.2024 12:54

    Забавно что я недавно как раз решил делать блог, но выбрал куда более простое решение:

    • Накидать статьи в формате markdown.

    • Сгенерировать статику на nextjs, используя unified для парсинга markdown в html.

    • Залить статику на GitHub Pages, бесплатно.

    • Комменты подключить позже с помощью gisgus.

    На все это статья вышла бы наверное меньше чем ваша.


    1. irvinerus
      27.12.2024 12:54

      Для второго пункта можно также использовать Hugo. Он генерирует html-статику блога как раз на основе markdown