Перейти к содержимому

Чистый pipeline для деплоя блога

Pipeline

Я давно хотел хостить блог самостоятельно и наконец-то сделал это. Главное, что меня останавливало до этого момента — это отсутствие опыта в DevOps и перфекционизм в том, как сделать все проще для себя.

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

С другой стороны, все сложное — это весело. Когда есть веселье, сложные вещи становятся проще, вы получаете и знания, и удовольствие одновременно.

Это история успеха о том, как я настроил continuous deployment для блога.

TODO

  • Создать сервер на удаленном VPS
  • Настроить HTTPS для домена с помощью Let's Encrypt
  • Настроить сервер один раз
  • Сделать так, чтобы было легко настроить на любой другой машине
  • Писать посты локально в markdown
  • Коммитить и пушить изменения в Github Repository
  • Публиковать изменения на сервер автоматически

Hugo

Я не буду объяснять здесь ничего специфичного для Hugo. Hugo — это генератор статических сайтов, который создает директорию public со всем содержимым сайта. Этот контент нужно раздавать. Я буду использовать Nginx для этого.

Dockerfile для сборки docker-образа из директории public с использованием Nginx.

FROM nginx:alpine
COPY ./public /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf # Эту строку можно опустить, если дефолтный nginx конфиг вам подходит
EXPOSE 80

У меня есть простой nginx.conf в корне проекта рядом с Dockerfile. Основная причина, по которой я переопределил этот конфиг — великолепная страница 404.

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    error_page 404 /404.html;
    location = /404.html {
        root   /usr/share/nginx/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Github actions

Я использую Github Actions как CI/CD инструмент для сборки и деплоя Docker-образа блога. Это простое и понятное решение, встроенное в Github.

Github Container Registry

Нам нужен Docker Registry для деплоя наших образов. Я использую Github Container Registry, который был представлен недавно. Вы можете использовать Docker Hub для той же цели.

Чтобы использовать GHCR, нам нужен Container Registry Personal Access Token. Чтобы создать этот токен, перейдите в Settings вашего пользователя Github, затем в Developer settingsPersonal access tokens, и нажмите Generate new token. После ввода пароля откроется эта страница. Отметьте эти чекбоксы и нажмите Generate token. Сохраните полученный токен для дальнейшего использования.

Token Rules

Secrets

Когда у вас есть что-то секретное для использования в Actions, вы должны добавить это как Secret, чтобы оно было зашифровано Github. Таким образом, вы можете создать свой Action без хардкода приватных данных.

Чтобы добавить строку Secret, перейдите на вкладку Settings, затем на страницу Secrets и нажмите кнопку New Secret.

Давайте добавим Secret с именем CR_PAT и вставим токен, полученный на предыдущем шаге.

Secrets

Теперь у нас есть все для написания workflow Github Actions.

Чтобы создать Action, нужно создать директорию .github/workflows с файлом action *.yaml.

Action file

Я создал workflow с именем pipeline.yml, используя все из предыдущих шагов.

name: Site Deploy # Имя Action. Может быть любым.

on:
  push:
    branches: [ develop ] # Выполнять эти jobs при любом push в ветку develop

jobs:
  pipeline: # Имя Job. Может быть любым.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2 # Checkout проекта

      - name: hugo
        uses: klakegg/actions-hugo@1.0.0 # Запуск hugo action для создания статической директории `public`
        with:
          env: production # Установка production env для разделения локальных проверок от production

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1 # Настройка QEMU для использования BuildX
        with:
          platforms: all

      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v1 # Настройка BuildX для использования Docker Push action
        with:
          version: latest

      - name: Login to Registry
        uses: docker/login-action@v1 # Настройка credentials для Docker registry
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }} # Использование имени репозитория как GHCR username
          password: ${{ secrets.CR_PAT }} # Container Registry Personal Access Token из secrets

      - uses: docker/build-push-action@v2
        with:
          context: . # Контекст текущей директории для создания образа
          file: ./Dockerfile # Использование Dockerfile из репозитория
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/pavelkorolevxyz-web # Docker image tag для push

Чтобы увидеть ваши actions, перейдите на вкладку Actions внутри репозитория.

Actions tab

Теперь, когда вы пушите в ветку develop, вы можете видеть лог action прямо на Github.

Actions tab

Docker

Мы будем использовать Docker Compose для запуска на VPS. Используя Docker Compose вместо обычного Docker, мы можем писать конфиги в удобных yaml-файлах, и таким образом можем связывать несколько контейнеров.

Настройка репозитория и установка Docker

Сначала нужно установить Docker на наш VPS. Все мои примеры для Ubuntu 20.04 VPS.

Настройка репозитория для загрузки docker.

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common \
    -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

Установка Docker.

sudo apt-get install docker-ce docker-ce-cli containerd.io

Больше информации в Официальном руководстве.

Установка Docker Compose

Для использования compose нужно установить его тоже.

sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Больше информации в Официальном руководстве.

Логин с помощью Github Access Token

export CR_PAT= # Установите ваш токен здесь
echo $CR_PAT | docker login ghcr.io -u username --password-stdin

Где $CR_PAT — это Github Access Token из Github Settings. GHCR username должен быть указан вместо username.

Watchtower

Хорошо. Теперь у нас все изменения пушатся в репозиторий и загружаются в registry без какой-либо помощи. Нам нужно что-то, чтобы получать эти измененные образы на нашем сервере.

Watchtower — безумно простой инструмент. Он просто периодически подтягивает последние образы из удаленного репозитория и может быть запущен как отдельный Docker-контейнер. Если последний образ и запущенный отличаются, то Watchtower перезапускает контейнер с новым образом. Давайте посмотрим, как это настраивается с помощью Docker Compose.

watchtower:
    image: containrrr/watchtower # Образ Watchtower
    restart: unless-stopped # Установить контейнер для перезапуска при выходе или перезапуске Docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock # Слушать изменения docker-контейнеров
      - /root/.docker/config.json:/config.json # Использовать config для получения credentials для pull из приватного репозитория
    command: --interval 60 # Запускать каждую минуту

Traefik

Я упоминал, что у меня домен pavelkorolev.xyz подключен к моему VPS. Сегодня это понятное желание — заставить его работать через протокол HTTPS, а также через HTTP.

Traefik — мощный reverse proxy и load balancer инструмент, но все, что нам нужно от Traefik здесь — это его поддержка Let's Encrypt.

Давайте суммируем наши намерения. Нам нужно запустить Traefik как Docker-контейнер на нашем сервере, настроить его на перенаправление на Nginx внутри контейнера блога и автоматически получать SSL-сертификаты.

Создать Docker proxy network. Она будет использоваться другими нашими контейнерами для нахождения в той же сети, что и Traefik, чтобы Traefik мог проксировать к ним.

docker network create proxy

Создать директорию traefik где угодно на сервере и директорию data внутри нее. Не обязательно создавать эту структуру директорий именно так, но мои сниппеты ниже подразумевают эту структуру и именование.

mkdir traefik
mkdir traefik/data

Создать пустой файл acme.json внутри директории data. Он будет заполнен позже самим Traefik, когда получит данные сертификатов.

touch traefik/data/acme.json

Создать конфигурацию traefik.yml внутри директории data.

nano traefik/data/traefik.yml

Заполнить traefik.yml.

api:
  dashboard: true

entryPoints:
  http:
    address: ":80"
  https:
    address: ":443"

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false

certificatesResolvers:
  http:
    acme:
      email: mail@example.com # ваш email
      storage: acme.json # ссылка на файл acme.json для записи
      httpChallenge:
        entryPoint: http

Создать credentials для dashboard Traefik. Заполните user и password, сохраните результирующую строку для дальнейшего использования.

echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g

Создать docker-compose.yml для образа Traefik в директории traefik.

nano traefik/docker-compose.yml

Заполнить docker-compose.yml.

version: '3'

services:
  traefik:
    image: traefik:v2.0 # Образ Traefik для использования
    container_name: traefik # Имя контейнера для запуска образа
    restart: unless-stopped # Установить контейнер traefik для перезапуска при выходе или перезапуске Docker
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy # Установить traefik для использования docker network, которую мы настроили ранее
    ports:
      - 80:80 # HTTP port mapping
      - 443:443 # HTTPS port mapping
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data/traefik.yml:/traefik.yml:ro # Файл конфигурации Traefik
      - ./data/acme.json:/acme.json # Файл сертификатов Traefik
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy" # Установить traefik для использования docker network, которую мы настроили ранее
      - "traefik.http.routers.traefik.entrypoints=http"
      - "traefik.http.routers.traefik.rule=Host(`traefik.example.com`)" # Установите ваш домен здесь
      - "traefik.http.middlewares.traefik-auth.basicauth.users=username:password" # Установите credentials dashboard traefik, полученные ранее
      - "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
      - "traefik.http.routers.traefik-secure.entrypoints=https"
      - "traefik.http.routers.traefik-secure.rule=Host(`traefik.example.com`)" # Установите ваш домен здесь
      - "traefik.http.routers.traefik-secure.middlewares=traefik-auth" # Установить dashboard traefik для защиты с помощью auth
      - "traefik.http.routers.traefik-secure.tls=true"
      - "traefik.http.routers.traefik-secure.tls.certresolver=http"
      - "traefik.http.routers.traefik-secure.service=api@internal"

networks:
  proxy: # Определить внешнюю сеть для запуска traefik
    external: true

Добавить права на запись в файл traefik/data/acme.json. Он используется для записи информации о сертификатах Let's Encrypt.

chmod 600 acme.json

Запустить Docker Compose из директории traefik.

cd traefik
docker-compose up -d

Конфигурация блога

Теперь у нас все настроено. Образ нашего блога — это единственное, для чего мы не создали конфиг.

Итоговый docker-compose.yml для проекта выглядит так.

version: "3"

services:
  web: # Конфигурация образа моего блога
    image: ghcr.io/pavelkorolevxyz/pavelkorolevxyz-web # расположение docker-образа в GHCR
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.web.entrypoints=http"
      - "traefik.http.routers.web.rule=Host(`example.com`)" # Ваш домен
      - "traefik.http.middlewares.web-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.web.middlewares=web-https-redirect"
      - "traefik.http.routers.web-secure.entrypoints=https"
      - "traefik.http.routers.web-secure.rule=Host(`example.com`)" # Ваш домен
      - "traefik.http.routers.web-secure.tls=true"
      - "traefik.http.routers.web-secure.service=web"
      - "traefik.http.services.web.loadbalancer.server.port=80"
    networks:
      - proxy
  watchtower: # Конфигурация Watchtower, которую я описал ранее
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /root/.docker/config.json:/config.json
    command: --interval 60

networks:
  proxy: # Установить внешнюю Docker network для использования
    external: true

Теперь можно запустить.

docker-compose up -d

Результат

В результате у меня есть:

  • Dockerfile для сборки образа блога.
  • Traefik docker-compose.yml с его конфигурацией traefik.yml и acme.json.
  • Основной docker-compose.yml для запуска блога с Watchtower, отслеживающим его изменения.

Эти файлы можно хранить в том же репозитории для дальнейшего использования (просто скопировать из репо на удаленный VPS при настройке). Основной бонус, который мы получили, используя Docker — это то, как легко мы можем развернуть ту же конфигурацию на любой другой машине.

TLDR настройка блога с нуля

  • Зарегистрировать домен
  • Создать VPS
  • Связать VPS с доменом
  • Создать репозиторий с блогом на Github
    • Если репозиторий приватный, нужна настройка Secret в Settings репо.
    • Создать Github Action для публикации Docker-образа блога в Github Container Registry
  • ssh на VPS
    • Установить Docker и Compose
    • Создать proxy network с помощью Docker
    • Если репозиторий приватный, нужна авторизация в GHCR
    • Скопировать конфиги для Traefik на VPS (из примеров выше)
    • Запустить compose Traefik
    • Скопировать конфиг compose для блога на VPS (из примеров выше)
    • Запустить compose блога

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

Достаточно ли это чисто? Я думаю, да.