Как запустить статический сайт в Яндекс облаке

12 Sep 2023 время чтения 8 мин Облако Georg Grauberger

В наше время сайты можно запускать везде, не всегда это удобно. Давайте посмотрим как можно запустить статический сайт в Яндекс Облаке и сделать это максимально удобным для публикации статей.

NOTE:
Гид предполагает что вы работаете с ОС Linux или MacOS. Если у вас Windows то советую попробовать WSL2

Для того чтобы следовать инструкциям вам понадобиться следующее.

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\&region=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\&paras\=5\&format\=text' >> content/post/hello-hugo.md

Теперь запустите сайт. Команда hugo server -D стартует сервер. Зайдя на http://localhost:1313 вы увидите следующее.

Rendered test site

В 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\&region=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, или текстовый редактор в браузере то статический сайт хороший вариант.