Заводить ли личный блог или сайт? Часть I. Готовим инфраструктуру c помощью Terraform

от автора

Полагаю, каждый разработчик рано или поздно приходит к мысли о том, что ему есть, что рассказать и чем поделиться. Кто-то даже начинает это делать в том или ином формате. И, конечно, хочется сказать спасибо всем тем, кто отвечает на вопросы на 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 канал.


ссылка на оригинал статьи https://habr.com/ru/articles/870366/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *