Полагаю, каждый разработчик рано или поздно приходит к мысли о том, что ему есть, что рассказать и чем поделиться. Кто-то даже начинает это делать в том или ином формате. И, конечно, хочется сказать спасибо всем тем, кто отвечает на вопросы на stackoverflow, пишет статьи или делает еще какой-либо контент. Однако быть автором труд весьма специфичный, всегда есть риск, что твой контент не будет полезен или даже интересен. За несколько лет мною было написано около пары десятков статей, а также было начато несколько своих проектов, но все это выглядит на первый взгляд как «работа в стол».
Однако, порефлексировав, я понял, что вся работа, которая не дает ожидаемого результата не напрасна. Как минимум — это опыт, а опыт штука полезная. Никогда не угадаешь, что тебе может пригодится в будущем. К тому же не стоит сбрасывать со счетов накопительный эффект общей массы знаний. Звучит довольно туманно, поэтому приведу пример. Разработка показала мне, как можно копить фрагментированные кусочки знаний долгое время, которые в определенный момент могут сложится в полную картину, а ты будешь удивляться, как не понимал чего-то раньше.
Так о чем это я? Я буду делать личный блог или сайт, на самом деле еще не знаю во что это выльется. Но как показал опрос в моем TG канале, у ребят, как и у меня, есть интерес к тому, как можно сделать и использовать блог, чтобы он приносил тебе пользу в каком-либо виде. Если дело пойдет, то здесь будет целая арка статей. Приступим!
С чего начать?
На самом деле в голосовании в своем канале, я обозначил более широкую тему: «Как сделать личный сайт, чтобы он в каком-то виде работал на тебя?». То есть речь шла о чем угодно, от блога до сайта студии по разработке. Но сделать что-то с таким широким выбором трудно, поэтому пообщавшись с ребятами, я решил начать с малого — личный блог.
Получается первое, что нужно сделать — определиться с форматом (блог, сайт студии и т.п.) и придерживаться его.
Первая мысль после определения курса — надо посмотреть референсы. Я собрал небольшой список, выложу и здесь. Вдруг кому-то будет интересно.
Критерии у меня были так себе — если сайт хоть как-то цеплял, то кидал в список. А когда посмотрел референсы, то начал думать о том, как реализовать нечто подобное.
То есть второй пункт — выбор инструментов.
Конечно сразу же приходит на ум такой инструмент, как Astro. На если использовать Astro и делать обычный статический блог, то с большой долей вероятности, здесь бы была только одна статья, а у меня бы появился блог, в который мне бы пришлось как-то привлекать трафик. И скорее всего — все это также бы пошло в стол. Поэтому я выбрал путь поинтереснее. А именно самостоятельно развернуть инфраструктуру, используя IaC подход, и написать динамический блог с cms на минималках, используя SSR.
Таким образом, я попробую «убить нескольких зайцев одним камнем». А именно:
-
Попрактиковаться в конфигурации облачной инфраструктуры.
-
Попробовать наконец-то SSR, а то для внутрекорпоративных продуктов он не в ходу.
-
Сделать блог.
Далее идет третий пункт — декомпозиция. На данном этапе я разделил задачу на три шага:
-
Написание конфигурации облачной инфраструктуры с помощью Terraform в облаке Selectel.
-
Настройка развернутой инфраструктуры с помощью Ansible.
-
Написание блога (Декомпозицию этого пункта проведу позже).
Немного о Terraform
А сейчас будет небольшой экскурс в Terraform для тех, кто с ним не работал, дабы предоставить возможность понимания хотя бы на уровне сущностей. Поэтому знающие могут пойти дальше к описанию модулей Terraform.
Terraform появился на свет благодаря HashiCorp и стал довольно популярным. Он хранит свой конфиг в файлах, написанных на HCL (ямлоподобные конфиги). При изменении файлов конфига Terraform автоматически определяет, что уже развернуто, а что следует добавить или удалить.
Terraform можно использовать для работы с любым REST API, правда, для API нужен особый плагин под названием провайдер. Полный список провайдеров для любых облачных сервисов можно найти в Terraform Registry. Кроме того, из Terraform Registry можно загрузить и готовые рецепты тех или иных сервисов — модули.
Обычно все облачные сервисы пишут подробные инструкции для работы с их API через Terraform. Вот некоторые из них:
К сожалению, сейчас, находясь в России, нельзя просто так взять и скачать себе Terraform. Вам потребуется VPN или зеркало. Если вы использовали зеркало, то определите в переменной PATH путь к бинарнику:
export PATH=$PATH:<path>
Установите Terraform, используя инструкцию из документации HashiCorp или эту инструкцию.
Теперь о работе с Terraform. Если кратко, то алгоритм работы следующий:
-
Определить, из каких элементов состоит инфраструктура.
-
Написать конфигурационные файлы.
-
terraform init
— установить плагины Terraform, необходимые для используемых элементов. -
terraform plan
— посмотреть, какие конкретно изменения Terraform внесёт в существующую инфраструктуру. -
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 можно поделить на три вида:
-
Input переменные.
-
Output переменные.
-
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 }
Локальные переменные полезны, когда в конфигурации есть многократное повторение одних и тех же значений.
Задавать значения переменных можно, используя дефолтные значения при их объявлении, как показано в примерах выше, а также можно использовать следующие методы:
-
В cli через -var (поштучно):
$ terraform apply -var="server_zone=ru-9a"
-
Через файл с расширением
.tfvars
(пачками). Создаём файл*.tfvars
:
server_zone=ru-9a
По умолчанию загружаются значения из terraform.tfvars
, но можно явно обозначить файл для загрузки:
$ terraform apply -var-file="testing.tfvars"
-
Через переменные окружения. Переменная должна начинаться с 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 определяет несколько мета-аргументов, которые можно использовать с любым типом ресурсов для изменения их поведения. К ним относятся:
-
depends_on — для указания скрытых зависимостей.
-
count — для создания нескольких экземпляров ресурсов, количество экземпляров = count. Подробности тут.
-
for_each создаёт несколько экземпляров модуля из одного блока модуля. За подробностями сюда.
-
provider — для выбора конфигурации провайдера. Дополнительно читать тут.
-
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/
Добавить комментарий