Как запустить статический сайт в Яндекс облаке
В наше время сайты можно запускать везде, не всегда это удобно. Давайте посмотрим как можно запустить статический сайт в Яндекс Облаке и сделать это максимально удобным для публикации статей.
NOTE:
Гид предполагает что вы работаете с ОС Linux или MacOS. Если у вас Windows то советую попробовать WSL2Для того чтобы следовать инструкциям вам понадобиться следующее.
- домен который обслуживается DNS серверами от yandex
- Account в Яндекс Облаке
- Установленный Яндекс cli
- github account
- установленный hugo
- установленный pulumi provider
NOTE:
DNS сервера yandex можно найти под ns1.yandexcloud.net и ns2.yandexcloud.net. Таким образом можно будет управлять записями из облака Яндекса.Настройка yandex cloud
Как уже сказано выше, я рассчитываю что yandex cloud уже установлен в системе,
но все еще нужно настроить его под наше облако.
Для этого есть команда интерактивной настройки.
Просто запустите yc init.
Pulumi понадобится бэкенд для сохранения состояния нашей инфраструктуры. Существуют разные варианты настройки. Первый и рекомендуемый это их собственный сервис, но так как я не люблю заводить новые логины мы не будем его использовать.
Второй вариант это json файл на вашей локальной файловой системе. Его можно
использовать в том случае если вы будете работать над своим сайтом только с
одного компьютера или просто хотите попробовать как работает pulumi.
Третий вариант, тот который мы будем использовать для этой статьи, это облачное объектное хранилище совместимое с апи S3.
Для начала понадобится создать такое хранилище с помощью команды yc которою мы
настроили прежде. Так как бакет должен иметь уникальное имя, желательно добавить
случайный стринг в конце названия. Иначе есть возможность что ваше имя будет уже
использовано.
yc storage bucket create --name your-bucket-name-$(echo $RANDOM | md5sum | head -c 8; echo;)
# Output
name: your-bucket-name-3482bfb2
folder_id: your-folder-id
anonymous_access_flags:
read: false
list: false
default_storage_class: STANDARD
versioning: VERSIONING_DISABLED
acl: {}
created_at: "2023-01-23T17:45:25.688169Z"
Сделав бакет, нам нужен пользователь который сможет писать в него. Так
называемый service-account которому мы должны присвоить access-key для
авторизации. Все эти операции, как и прежде, можно сделать с помощью командной
строки.
NOTE:
Из нижеупомянутых команд сохраняем следующие значения.
- id сервисного пользователя
- folder_id
- key_id
- secret
yc iam service-account create --name pulumi-backend-storage
id: aje6lk6ov7u04njl6er6
folder_id: b1glrb61es8if4qbp6j7
created_at: "2023-01-23T18:23:01.681284934Z"
name: pulumi-backend-storage
# Id берем из предыдущий команды
yc iam access-key create --service-account-id aje6lk6ov7u04njl6er6
access_key:
id: ajepic6sdcs39dpoltfp
service_account_id: aje6lk6ov7u04njl6er6
created_at: "2023-01-23T18:26:04.538821120Z"
key_id: YCAJENQtJ0mOaQL_HpwaKGmz2
secret: YCP2YXpYEswXdc0envcb9XY5g2IShgDA-pJizbC4
Осталось добавить пользователю роль администратора хранилища. Так как в Яндекс
облаке роль можно назначить только на все облако или на одну папку в облаке,
нужно запускать следующую команду. Id после add-access-binding это id нашей
папки в которой мы создали сервисного пользователя.
yc resource-manager folder add-access-binding b1glrb61es8if4qbp6j7 --role storage.admin --subject serviceAccount:aje6lk6ov7u04njl6er6
Теперь облако готово для работы с Pulumi.
Pulumi
Для работы с Pulumi нужно будет инициализировать папку для нашего проекта и произвести логин в выбранный нами бэкенд. Pulumi предлагает несколько языков для работы с ресурсами в Яндекс облаке. Я предпочитаю python, поэтому продолжим именно с ним.
Небольшое пояснение. Для работы с yandex object storage нам надо будет использовать переменные которые являются переменными для AWS S3. Так что не удивляйтесь их именам.
export AWS_ACCESS_KEY_ID=значение из команды создания ключа
export AWS_SECRET_ACCESS_KEY=значение из команды создания ключа
pulumi login s3://имя-вашего-бакета\?endpoint=https://storage.yandexcloud.net\®ion=ru-central1-a
# Создаем новую папку и заводим там новый проект с pulumi.
mkdir site-infrastructure && cd site-infrastructure
pulumi new python
После ответа на вопросы в вашей папке должна быть такая структура.
tree -L 1
.
├── __main__.py
├── Pulumi.dev.yaml
├── Pulumi.yaml
├── requirements.txt
└── venv
Добавьте еще одну зависимость. Это pulumi-yandex-unofficial. Неофициальный
т.к. из за СВО pulumi отказались дальше поддерживать свой официальный бэкенд.
Добавьте его следующим образом.
source venv/bin/activate
pip install pulumi-yandex-unofficial
Вся последовательная работа будет происходить внутри файла __main__.py.
Для установки сайта через Pulumi требуются следующие облачные ресурсы.
| Название ресурса | Предназначение |
|---|---|
| static site catalog | Папка в который будут находится все ресурсы |
| deployment_user | Пользователь который будет публиковать наш сайт |
| service-user-static-access-key | Ключи доступа для пользователя |
| role | Роли которые будут присвоены пользователю |
| role-binding | Мета сущность которая определяет какие роли присвоены какому пользователю |
| folder-editor-role | Роль позволяющая писать в папку |
| certificate | Сертификат для безопасности сайта |
| site-bucket | Бакет в который будем загружать сайт |
| dns-zone | ДНС зона для сайта, в ней будут все именные записи вашего домена |
| dns-challenge-record | Запись для подтверждения собственности домена, нужна чтобы получить сертификат |
| dns-record | Запись на которую будут ходить ваши посетители |
С Pulumi можно создать все эти ресурсы программатическим образом.
import pulumi
import pulumi_yandex_unofficial as yc
site_domain_name = "chipnibbles.ru"
static_site_catalog = yc.ResourcemanagerFolder(
"static-site"
)
deployment_user = yc.IamServiceAccount(
"github-actions-user",
description="Service user for yc to deploy static site",
folder_id=static_site_catalog.id,
)
service_user_static_access_key = yc.IamServiceAccountStaticAccessKey(
"github-actions-access-key",
service_account_id=deployment_user.id,
description="Static access key for deployment user. Used for object storage",
)
roles = ["storage.admin"]
role_bindings = []
for role in roles:
role_bindings.append(
yc.IamServiceAccountIamBinding(
f"{role}-binding",
members=[pulumi.Output.concat("serviceAccount:", deployment_user.id)],
role=role,
service_account_id=deployment_user.id,
)
)
folder_editor_roles = yc.ResourcemanagerFolderIamMember(
"folder-editor",
folder_id=static_site_catalog.id,
role="storage.editor",
member=deployment_user.id.apply(lambda id: f"serviceAccount:{id}"),
)
certificate = yc.CmCertificate(site_domain_name.replace(".", "-"),
domains=[site_domain_name],
folder_id=static_site_catalog.id,
managed=yc.CmCertificateManagedArgs(
challenge_type="DNS_CNAME",
)
)
site_bucket = yc.StorageBucket(
site_domain_name,
access_key=service_user_static_access_key.access_key,
secret_key=service_user_static_access_key.secret_key,
bucket=site_domain_name,
acl="public-read",
https=yc.StorageBucketHttpsArgs(
certificate_id=certificate.id
),
website=yc.StorageBucketWebsiteArgs(
error_document="error.html",
index_document="index.html",
),
)
dns_zone = yc.DnsZone(
site_domain_name.replace(".", "-"),
description="Zone for the chipnibbles-ru static site",
folder_id=static_site_catalog.id,
zone=f"{site_domain_name}.",
public=True,
)
dns_challenge_record = yc.DnsRecordSet(
"cert_challenge",
zone_id=dns_zone.id,
ttl=200,
type=certificate.challenges[0].dns_type,
name=certificate.challenges[0].dns_name,
datas=[certificate.challenges[0].dns_value]
)
dns_record = yc.DnsRecordSet(
f"{site_domain_name}.",
name=f"{site_domain_name}.",
zone_id=dns_zone.id,
type="ANAME",
ttl=200,
datas=[site_bucket.website_endpoint],
)
pulumi.export("Static-Access-Key-Var", service_user_static_access_key.access_key.apply(
lambda access_key: f"AWS_ACCESS_KEY_ID={access_key}"
))
pulumi.export("Static-Access-Key-Secret-Var", service_user_static_access_key.secret_key.apply(
lambda secret_key: f"AWS_SECRET_ACCESS_KEY={secret_key}"
))
pulumi.export("Catalog Link", static_site_catalog.id.apply(
lambda id: f"https://console.cloud.yandex.ru/folders/{id}"
))
Этот код довольно простой, я всего лишь обращу внимание на ресурсы certificate
и dns_zone. В них не допускаются точки в значении имени ресурса поэтому я их
заменил дефисом. Учтите что название бакета и сайта должны совпадать. Код это
предусматривает, но если вы будете повторять его сами, то об этом стоит знать.
Осталось запустить инфраструктуру.
pulumi up
В выводе команды вы получите новые ключи доступа которые нам понадобятся в следующем отделе.
Hugo
Для создания нового сайта в hugo предусмотрена команда
hugo new site имя-вашего-сайта таким образом hugo создаст новую папку с
аналогичным именем. В ней следующее содержание.
.
├── archetypes
│ └── default.md
├── assets
├── config.toml
├── content
├── data
├── layouts
├── public
├── static
└── themes
Создайте базовую настройку сайта. Для этого нужно прописать параметры в файле
config.toml, добавить дизайн сайта и установить golang.
Установить golang можно по инструкции на официальном сайте.
Теперь настроим hugo для использования модулей и добавим репозиторий с
дизайном ananke.
hugo mod init github.com/имя-пользователя/имя-сайта
# Добавляем строку в config.toml
echo "theme = ['github.com/theNewDynamic/gohugo-theme-ananke']" >> config.toml
Для наглядности добавим еще одну статью. И заполним ее шаблонным текстом.
hugo new post/hello-hugo.md
curl 'https://baconipsum.com/api/\?type\=meat-and-filler\¶s\=5\&format\=text' >> content/post/hello-hugo.md
Теперь запустите сайт. Команда hugo server -D стартует сервер. Зайдя на
http://localhost:1313 вы увидите следующее.

В hugo есть команда hugo deploy которая загружает генерированный сайт в бакет.
Необходимые ключи авторизации hugo считывает из файла ~/.aws/credentials.
Добавляем наши ключи в следующем формате в этот файл.
[default]
aws_access_key_id = id из команды pulumi up
aws_secret_access_key = секрет из команды pulumi up
И прописываем настройки бакета в config.toml.
[[deployment.targets]]
name = "yandex-cloud"
URL = "s3://имя-вашего-бакета-для-pulumi?region=ru-central1-a&endpoint=https://storage.yandexcloud.net"
Также убираем статус наброска из статьи и запускаем команду.
# В файле content/post/hello-hugo.md меняем четвертую строку на
draft: false
# И запускаем команду
hugo deploy
Сайт доступен под https://имя-вашего-бакета
Теперь можно писать статьи и добавлять новую инфраструктуру к вашему проекту. Но
для публикации все еще надо будет запускать несколько команд вручную.
Мы ленивые и не хотим повторять одну и ту же тривиальную работу, мы можем
воспользоваться технологией github actions и компьютер будет исполнять эту
работу за нас.
Github Actions
Github Actions это механизм постоянной интеграции и доставки (CI/CD) каждый раз, в зависимости от настроек, когда мы будем фиксировать новое состояние кода github будет запускать программу на изолированной виртуальной машине.
Для настройки этого функционала нужно добавить новые папки под названием
.github/workflows в наш репозиторий и создать в нем новый файл deploy.yml с
следующим содержанием.
name: build-chipnibbles-site
on: push
jobs:
provision-infrastructure:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: infrastructure
container:
image: pulumi/pulumi
options: --user root
env:
YC_TOKEN: ${{ secrets.YC_TOKEN }}
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
path: .
- name: init venv
run: |
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
- name: Provision infrastructure
run: |
pulumi login s3://${{ vars.STATE_BUCKET }}\?endpoint=https://storage.yandexcloud.net\®ion=ru-central1-a
pulumi up --non-interactive -s prod -y
- name: Create secret vars
run: |
echo "::add-mask::$GEN_ACCESS_KEY_ID_VAR"
echo "::add-mask::$GEN_ACCESS_KEY_SECRET_VAR"
GEN_ACCESS_KEY_ID_VAR=$(pulumi stack output -s prod --show-secrets Static-Access-Key-Var)
GEN_ACCESS_KEY_SECRET_VAR=$(pulumi stack output -s prod --show-secrets Static-Access-Key-Secret-Var)
echo "$GEN_ACCESS_KEY_ID_VAR" >> $GITHUB_ENV
echo "$GEN_ACCESS_KEY_SECRET_VAR" >> $GITHUB_ENV
- name: Register Access Key ID
uses: dacbd/gha-secrets@v1
with:
token: ${{ secrets.GH_PAT }}
name: DEPLOY_AWS_ACCESS_KEY_ID
value: ${{ env.DEPLOY_AWS_ACCESS_KEY_ID }}
- name: Register Access Key ID
uses: dacbd/gha-secrets@v1
with:
token: ${{ secrets.GH_PAT }}
name: DEPLOY_AWS_SECRET_ACCESS_KEY
value: ${{ env.DEPLOY_AWS_SECRET_ACCESS_KEY }}
build-chipnibbles-site:
runs-on: ubuntu-latest
needs: provision-infrastructure
container:
image: cr.yandex/crpp8tmsg2hn2jguoorb/hugo:latest
options: --user root
env:
AWS_ACCESS_KEY_ID: ${{ secrets.DEPLOY_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DEPLOY_AWS_SECRET_ACCESS_KEY }}
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Build Site
run: hugo --minify
- name: Deploy site
run: hugo deploy
Теперь с каждым новым коммитом github actions будет запускать pulumi и после собирать наш сайт с помощью hugo. Чтобы эти скрипты работали надо создать секреты.
Зайдите в настройки вашего репозитория. Под пунктами Security -> Secrets and variables -> Actions добавьте следующие переменные.
# Креды для бакета с состоянием pulumi
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
# Персональный токен для Github
GH_PAT
# Пароль которые вы использовали для шифрования состояния в pulumi
PULUMI_CONFIG_PASSPHRASE
# Токен для доступа в Яндекс Облако
YC_TOKEN
Итог
Техническая часть запуска сайта закончилась, осталось только писать и публиковать статьи когда хотите. О стоимости этого сайта не стоит долго задумываться, например сейчас я плачу максимум 4 рубля в месяц за chipnibbles.ru.
Поэтому если вам нужна страница в интернете, но при этом не нужен динамический функционал wordpress, или текстовый редактор в браузере то статический сайт хороший вариант.