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

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