Полагаю, каждый разработчик рано или поздно приходит к мысли о том, что ему есть, что рассказать и чем поделиться. Кто-то даже начинает это делать в том или ином формате. И, конечно, хочется сказать спасибо всем тем, кто отвечает на вопросы на 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 канал.
Комментарии (7)
MAXH0
27.12.2024 12:54Всегда делай то, что собрался делать! Если собрался завести блог, надо ЗАВЕСТИ БЛОГ. Не программировать его. Не настраивать серверные части. А именно - завести блог. Простейшим способом. Вордпресс, генератор статики, любой сервис - без разницы. Садишься и пишешь свой Веб лог...
Сам всегда делал эту ошибку. Она стоила мне 10 лет пробуксовки когда я шлифовал кирпич пытаясь довести до совершенства то, чем в итоге и не стал пользоваться.
Goodzonchik
27.12.2024 12:54Полностью согласен, я бы даже добавил еще от себя на счет обучения технологиям. Не надо придумывать проект, который должен будет принести профит и деньги, надо просто взять и сделать учебный проект с синтетическими данными.
Для блогов есть уже много готовых ресурсов, особенно для начинающего блогера завести ТГ, ВК - вполне неплохой вариант, чем сделать сайт, на который нужно еще как-то нагнать трафик.
Для остального есть докер и гитхаб)
dsoastro
27.12.2024 12:54Какой смысл для настройки сервака для блога использовать терраформ и потом ансибл? Если это условно однократное событие руками проще на порядок сделать. Ансибл хоть как-то можно понять - при переезде к другому провайдеру плейбук менять не придется, но код терраформа специфичен для провайдера. Стрельба из пушек по воробьям
MrAlone
27.12.2024 12:54Когда девелопер вдруг решает, что он может в инфраструктуру, получается вот такое.
gen1lee
27.12.2024 12:54Забавно что я недавно как раз решил делать блог, но выбрал куда более простое решение:
Накидать статьи в формате markdown.
Сгенерировать статику на nextjs, используя unified для парсинга markdown в html.
Залить статику на GitHub Pages, бесплатно.
Комменты подключить позже с помощью gisgus.
На все это статья вышла бы наверное меньше чем ваша.
irvinerus
27.12.2024 12:54Для второго пункта можно также использовать Hugo. Он генерирует html-статику блога как раз на основе markdown
ExternalWayfarer
Не зайдём :)