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

Есть два наиболее часто встречающихся варианта веб-сайта, с которыми я сталкивался: сайт без серверной логики, сайт с серверной логикой. В этой статье я опишу архитектуры для реализации этих видов сайтов. Эти архитектуры хорошо подходят для моих задач (пет-проекты с нулевым бюджетом), но могут не подойти для бизнес-ориентированных сайтов. Справедливости ради, скажу, что они могут, но не обязаны подойти для бизнес-ориентированных приложений.

В качестве демонстрации, я предлагаю рассмотреть два проекта. Назовем их следующим образом: Проект-1 и Проект-2.

Требования к Проекту-1

  • Пользователь может зайти на веб-сайт и увидеть статическую информацию.

Требования к Проекту-2

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

Архитектурные характеристики

Так как это пет-проекты, не предназначенные для зарабатывания денег, в обоих случаях для нас важны следующие архитектурные характеристики:

  • Низкая стоимость хостинга (в идеале бесплатно).
  • Простота реализации и развертывания.
  • Простота поддержки (в идеале, вообще не требует поддержки после запуска).
  • Базовая защищенность веб-сайта.
  • Сайт легко найти в интернете.

Разберем по порядку решения для этих проектов.

Проект-1

Учитывая требования, самым простым решением будет статический веб-сайт. Я обычно использую Vue для SPA и Astro для SSG. В обоих случаях результатом сборки будет папка с пачкой файлов, которые надо положить на веб-сервер.

Далее я рассмотрю два варианта в качестве платформы для развертывания нашего сайта.

Первый вариант: Timeweb Cloud

Размещаем сайт на хостинге Timeweb Cloud (смотри раздел Apps). На момент написания этой статьи статический сайт на этой платформе обходится мне примерно в 1₽ в месяц. Бонусом идет автоматически настроенный CI с перенакаткой сайта по коммиту в мастер ветку в GitHub. Здесь также можно подключить собственный домен. Бесплатный сертификат Let’s Encrypt выпускается и обновляется автоматически. Данное решение удовлетворяет вообще всем нашим архитектурным требованиям. Единственная проблема - сертификаты иногда не успевают обновляться и сайт становится недоступным. Не знаю, кто в этом виноват, но решается это быстро, путем переподключения домена к сайту через админку Timeweb.

Второй вариант: Yandex Object Storage

Размещаем сайт на хостинге Yandex Object Storage. Для этого выполняем следующие действия:

  • Создаем бакет с именем, повторяющим имя домена. Имя бакета может состоять только из латинских букв. Поэтому, если у нас кириллический домен, то надо назвать бакет в punycode кодировке.
  • Создаем сертификат Let’s Encrypt для нашего домена в Yandex Certificate Manager. После того, как домен верифицирован и сертификат выпущен, нам больше не надо думать об обновлении сертификатов. Облако берет это на себя.
  • Идем в настройки бакета в раздел “Безопасность - HTTPS” и прикрепляем наш новый домен к бакету.
  • Добавляем DNS записи для нашего домена, чтобы домен резолвился в бакет. Нюанс в том, что у бакета нет фиксированного IP адреса, у него есть только служебный домен. В случае, если мы хотим расположить сайт на поддомене (например, example.mysite.com), это не проблема, т.к. мы можем создать CNAME запись у нашего администратора домена, которая будет вести на служебный домен бакета. Если же мы хотим расположить сайт на корневом домене (например, mysite.com), и наш DNS администратор не поддерживает ANAME записи, то придется делегировать наш домен сервису Yandex Cloud DNS. Для этого идем в админку нашего DNS администратора и прописываем сервера яндекса в качестве DNS серверов. Переезд может занять до суток. Далее идем в Yandex Cloud DNS и создаем записи для нашего домена. На момент написания статьи, оплата за домен почасовая. Это единственная статья расходов в Yandex Cloud для моего проекта. В месяц выходит около 40₽. Но, напомню, если нам нужно разместить сайт на поддомене (ограничившись CNAME записью), то все эти манипуляции с Yandex Cloud DNS не требуются, и эта статья расходов не появится.
  • Если у вас SPA, то в настройках сайта надо прописать error page: index.html.

Загрузка файлов в Yandex Object Storage

Бакет создан, теперь нужно загрузить в него файлы сайта. Можно сделать это руками через веб интерфейс Object Storage. Но лучше воспользоваться cli, т.к. это позволит нам быстро и удобно вносить изменения в сайт в будущем.

Сначала устанавливаем aws cli.

Да, это не опечатка. Мы загружаем файлы в яндекс бакет с помощью aws cli.

В ~/.aws/config прописываем регион и эндпоинт:

[default]
region = ru-central1
endpoint_url = https://storage.yandexcloud.net

В Yandex IAM создаем сервисный аккаунт и назначаем ему роль storage.editor. Далее для этого аккаунта создаем пару ключей (“Создать статический ключ доступа”).

В ~/.aws/credentials прописываем только что созданные ключи:

[default]
aws_access_key_id = <id value>
aws_secret_access_key = <secret value>

Далее можно загружать файлы в Object Storage с помощью следующего скрипта:

ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd ROOT_DIR
DIST_DIR="./dist/"

npm ci
npm run build

source ./.yandex-cloud.env

aws s3 sync --delete $DIST_DIR s3://$BUCKET_NAME/

echo "Файлы загружены в Object Storage"

Сравнение двух вариантов развертывания Проекта-1

Выше я приводил архитектурные характеристики, которым должно соответствовать наше решение. Сравним два варианта развертывания на основе этих характеристик.

Оценка варианта развертывания Проекта-1 в Timeweb Cloud с точки зрения соответствия архитектурным характеристикам. Оценка по пятибалльной шкале: ★ - плохо, ★★★★★ - отлично.
Низкая стоимость хостинга ★★★★★ 1 ₽ в месяц - почти бесплатно
Простота реализации и развертывания ★★★★★ Интуитивно в пару кликов создается и развертывается приложение
Простота поддержки ★★★★ Иногда слетают сертификаты, зато настроен CI
Базовая защищенность веб-сайта ★★★★★ Сертификат Let’s Encrypt прилагается
Сайт легко найти в интернете ★★★★★ Можно подключить свой домен
Оценка варианта развертывания Проекта-1 в Yandex Object Storage с точки зрения соответствия архитектурным характеристикам. Оценка по пятибалльной шкале: ★ - плохо, ★★★★★ - отлично.
Низкая стоимость хостинга ★★★★ Если надо делегировать домен в Yandex Cloud DNS, то появится статья расходов примерно в 40 ₽ в месяц
Простота реализации и развертывания ★★★ Быстро получится только, если знать, что делать. Не интуитивно. Много действий
Простота поддержки ★★★★ После первоначальной настройки работать будет вечно, без вашего участия. Но CI из коробки не идет. Поэтому деплой настраиваем сами
Базовая защищенность веб-сайта ★★★★★ Сертификат Let’s Encrypt прилагается
Сайт легко найти в интернете ★★★★★ Можно подключить свой домен

Проект-2

Учитывая требования, нам нужен веб-сайт с серверной логикой. В Timeweb Cloud можно развертывать бэкенд так же просто, как и фронтенд. Но стоить это будет уже существенно дороже, чем статический веб сайт (сотни рублей в месяц вместо единиц рублей). Поэтому, учитывая требования жесткой экономии, я выбрал Yandex Cloud.

Для реализации этого проекта нам нужен небольшой бэкенд, который будет принимать форму и вызывать API Teleram для отправки данных в телеграм.

Самый простой и дешевый вариант (при количестве пользователей, стремящемся к нулю) - это Yandex Cloud Function.

Веб-интерфейс сайта располагается в Object Storage (так же, как и в Проекте-1). Чтобы склеить веб-интерфейс и Cloud Function в единый сайт, мы используем сервис Yandex API Gateway.

Архитектура нашего веб-сайта

В админке Yandex API Gateway создаем шлюз. Этот шлюз настраивается yaml файлом. Для перенаправления запросов в Object Storage добавляем в конфиг интеграцию с Object Storage. А для перенаправления запросов в Cloud Function - интеграцию с Cloud Function. Должен получиться примерно такой конфиг:

openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
servers:
  - url: <технический урл>
  - url: <публичный урл>
paths:
  /:
    get:
      x-yc-apigateway-integration:
        bucket: <имя бакета со статическим сайтом>
        error_object: index.html
        type: object_storage
        service_account_id: <айди сервисного аккаунта с правами на чтение бакета>
        object: 'index.html'
  /{file+}:
    get:
      parameters:
        - name: file
          in: path
          required: false
          schema:
            type: string
      x-yc-apigateway-integration:
        bucket: <имя бакета со статическим сайтом>
        error_object: index.html
        type: object_storage
        service_account_id: <айди сервисного аккаунта с правами на чтение бакета>
        object: '{file}'
  /api/{path+}:
    x-yc-apigateway-any-method:
      parameters:
        - name: path
          in: path
          description: backend path
          required: true
          schema:
            type: string
      x-yc-apigateway-integration:
        type: cloud_functions
        function_id: <айди облачной функции с нашим бэкендом>
        tag: "$latest"
        service_account_id: <айди сервисного аккаунта с правами на вызов облачной функции>

В результате, запросы на /api/* будут направлены в Cloud Function, а все остальные запросы пойдут на Object Storage.

Код отправки веб формы на фронте выглядит примерно так:

const sendMessage = async (name, message) => {
	const res = await fetch("/api/send-message", {
		method: "POST",
		headers: {
			"Content-Type": "application/json",
		},
		body: JSON.stringify({ name, message }),
	});
	return res.ok;
};

Деплоится фронт так же, как и в Проекте-1.

На бэкенде, Cloud Function принимает объект event в качестве аргумента. В этом объекте находятся все параметры запроса (заголовки, метод, тело и т.д.). Проект у нас небольшой, поэтому нам не понадобится мощный бэкенд-фреймворк. Все запросы мы будем разбирать вручную, без использования стороннего роутера.

Код обработчика в Cloud Function выглядит примерно так:

import { sendMessage } from "../service/sendMessage.js";

const handleSendMessage = async (event) => {
	const body = JSON.parse(event.body);
	await sendMessage(body.name, body.message);

	return {
		statusCode: 200,
	};
};

export const handler = async (event) => {
	const method = event.httpMethod;
	const [path] = event.url.split("?");

	if (method === "POST" && path === "/api/send-message") {
		return handleSendMessage(event);
	}

	return {
		statusCode: 404,
	};
};

Для деплоя бэкенда нам понадобится yc cli, установим её по инструкции.

Далее используем примерно такой скрипт:

ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd $ROOT_DIR
DIST_DIR="./yandexCloudFnDist"
FUNCTION_ARCHIVE_PATH="./yandexCloudFn.zip"

npm --omit=dev ci

if [[ -d $DIST_DIR ]]; then
  echo "Удаление существующей директории ${DIST_DIR}..."
  rm -rf $DIST_DIR
fi

rm -f $FUNCTION_ARCHIVE_PATH

echo "Создание новой директории ${DIST_DIR}..."
mkdir $DIST_DIR

echo "Перемещение исходных файлов в ${DIST_DIR}..."
cp -R \
  package.json \
  node_modules/ \
  src/ \
  $DIST_DIR/

echo "Архивация содержимого директории ${DIST_DIR}..."
zip -r $FUNCTION_ARCHIVE_PATH $DIST_DIR

echo "Yandex Cloud Function собрана"

source ./.yandex.cloud.env

echo "Загрузка Yandex Cloud Function в облако..."

yc serverless function version create \
  --function-name="${FUNCTION_NAME}" \
  --runtime nodejs22 \
  --entrypoint "yandexCloudFnDist/src/controller/yandexCloudFunction.handler" \
  --memory 128m \
  --execution-timeout 5s \
  --environment TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN}" \
  --environment TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID}" \
  --source-path ./yandexCloudFn.zip

Обратите внимание, что мы скипаем установку dev зависимостей. Это очень важно, т.к. Cloud Function имеет очень небольшой лимит на размер функции, и, если засунуть в нее все dev зависимости, то скорее всего этот лимит будет превышен.

Сравнение двух вариантов развертывания Проекта-2

Выше я приводил архитектурные характеристики, которым должно соответствовать наше решение. Сравним два варианта развертывания на основе этих характеристик.

Оценка варианта развертывания Проекта-2 в Timeweb Cloud с точки зрения соответствия архитектурным характеристикам. Оценка по пятибалльной шкале: ★ - плохо, ★★★★★ - отлично.
Низкая стоимость хостинга ★★ Из-за бэкенда стоимость увеличивается до сотен рублей в месяц
Простота реализации и развертывания ★★★★★ Интуитивно в пару кликов создается и развертывается приложение
Простота поддержки ★★★★ Иногда слетают сертификаты, зато настроен CI
Базовая защищенность веб-сайта ★★★★★ Сертификат Let’s Encrypt прилагается
Сайт легко найти в интернете ★★★★★ Можно подключить свой домен
Оценка варианта развертывания Проекта-2 в Yandex Object Storage с точки зрения соответствия архитектурным характеристикам. Оценка по пятибалльной шкале: ★ - плохо, ★★★★★ - отлично.
Низкая стоимость хостинга ★★★★ Если надо делегировать домен в Yandex Cloud DNS, то появится статья расходов примерно в 40 ₽ в месяц
Простота реализации и развертывания ★★ Быстро получится только, если знать, что делать. Не интуитивно. Много действий. Еще сложнее, чем статический сайт в Object Storage
Простота поддержки ★★★★ После первоначальной настройки работать будет вечно, без вашего участия. Но CI из коробки не идет. Поэтому деплой настраиваем сами
Базовая защищенность веб-сайта ★★★★★ Сертификат Let’s Encrypt прилагается
Сайт легко найти в интернете ★★★★★ Можно подключить свой домен

Полный исходный код Проекта-2 можно посмотреть здесь. Живой пример развернутого Проекта-2 можно посмотреть здесь.

Смотри также