Привет, Habr!
Heroku меньше чем через месяц станет недоступен для бесплатного использования. У многих моих знакомых, и у меня в том числе, на нем хостились несколько проектов. Поэтому встал вопрос миграции в другое облако. Остановился на Яндекс Облаке:
Относительно невысокие цены.
Возможность управлять как из панели, так и через CLI.
Интеграция с GitHub Actions.
Пробный период.
Задача
Есть 2 приложения: frontend и backend. Нужно выложить их на хостинг вместе. А чтобы работали вместе, склеить их с помощью nginx.
Общий вид пайплайна
Пуш изменений в main.
Создание Docker образов.
Пуш образов в Docker Hub.
Уведомление VM об обновлениях.
Создание Docker образов
Используем GitHub Actions для запуска нашего пайплайна при пуше в main.
on:
push:
branches: [ "main" ]
Теперь настроим билд Docker образов.
Т.к. у меня 2 отдельных приложения (React, FastAPI) + связующее звено (nginx), то билдить лучше параллельно.
env:
FRONTEND_IMAGE: ${{ secrets.DOCKER_HUB_USERNAME }}/text-to-image-frontend:${{ github.sha }}
BACKEND_IMAGE: ${{ secrets.DOCKER_HUB_USERNAME }}/text-to-image-api:${{ github.sha }}
NGINX_IMAGE: ${{ secrets.DOCKER_HUB_USERNAME }}/text-to-image-nginx:${{ github.sha }}
jobs:
build-backend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Dockerhub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Setup Buildx
uses: docker/setup-buildx-action@v2
- name: Build Backend
uses: docker/build-push-action@v3
with:
context: ./src/backend
file: ./src/backend/Dockerfile
push: true
tags: ${{ env.BACKEND_IMAGE }}
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Dockerhub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Setup Buildx
uses: docker/setup-buildx-action@v2
- name: Build Frontend
uses: docker/build-push-action@v3
with:
context: src/frontend
file: ./src/frontend/Dockerfile
push: true
build-args: SERVER_URL=${{ secrets.API_SERVER_URL }}
tags: ${{ env.FRONTEND_IMAGE }}
build-nginx:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Dockerhub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Setup Buildx
uses: docker/setup-buildx-action@v2
- name: Build Nginx for together composing
uses: docker/build-push-action@v3
with:
context: ./nginx
file: ./nginx/Dockerfile
push: true
tags: ${{ env.NGINX_IMAGE }}
* Названия создаваемых образов содержат хеш коммита вместо 'latest'. Причина в том, что обновление контейнера не будет происходить, если тег не обновился. Поэтому не стоит использовать неизменные теги.
Уведомление VM
Чтобы контейнеры в VM обновились, ее нужно уведомить. Будем использовать готовый Action для деплоя.
О создаваемой VM
Это действие создает (или обновляет существующую) виртуальную машину на базе COI (Container Optimized Image).
Для нас это означает, что это Ubuntu с предустановленными Docker и специальным демоном для его запуска.
В экшене пока доступен только docker-compose, хотя есть вариант COI спецификации
На этом шаге нам нужно передать:
Ключ для сервисного аккаунта.
Метаданные виртуальной машины.
Docker-compose спецификацию.
Информацию о пользователях.
Ключ сервисного аккаунта
Виртуальные машины создает сервисный аккаунт. Чтобы действовать от лица этого сервисного аккаунта нужно передать его ключ.
Создать этот ключ можно через CLI
yc iam access-key create --service-account-name sample-sa-name
Передавать ключ через параметр yc-sa-json-credentials
* Для сервисного аккаунта необходима роль compute.admin
, чтобы он мог создавать и обновлять виртуальные машины
Метаданные VM
Для создания виртуальной машины нужно передать ее описание через параметры:
folder-id
- ID каталога, где будет располагаться VM (обязательно);vm-name
- название виртуальной машины (обязательно);vm-subnet-id
- ID подсети, в которой будет располагаться VM (обязательно);vm-service-account-id
- ID сервисного аккаунта, ассоциированного с VM;vm-service-account-name
- название сервисного аккаунта, если ID не передан;vm-cores
- количество ядер процессора VM;vm-memory
- размер RAM VM;vm-core-fraction
- для vCPU в процентах;vm-disk-size
- размер диска;vm-zone-id
- ID зоны, в которой будет располагаться VM;vm-platform-id
- ID платформы (процессора, грубо говоря).
Первые 3 - обязательные, для остальных есть значения по-умолчанию
Чтобы найти подходящие конфигурации можно собрать свою в конфигураторе цен, а затем найти требуемые ID в документации. Например, для Intel Broadwell -vm-platform-id: 'standard-v1'
Передача docker-compose спецификации
Контейнеры будем создавать с помощью docker-compose. Путь к файлу спецификации передается через параметр docker-compose-path
.
К этому файлу будет применяться шаблонизатор Mustache. Т.е. мы можем передавать ему параметры через переменные окружения, а для доступа к ним использовать синтаксис: {{ env.VARIABLE_NAME }}
.
Например, так передаются названия создаваемых Docker образов и другие переменные
update-yc:
- name: Deploy COI VM
uses: yc-actions/yc-coi-deploy@v1.0.1
env:
BACKEND_IMAGE: ${{ env.BACKEND_IMAGE }}
FRONTEND_IMAGE: ${{ env.FRONTEND_IMAGE }}
NGINX_IMAGE: ${{ env.NGINX_IMAGE }}
FRONTEND_ORIGINS: ${{ secrets.FRONTEND_ORIGINS }}
NGINX_CERT: ${{ secrets.NGINX_CERT }}
NGINX_CERT_KEY: ${{ secrets.NGINX_CERT_KEY }}
with:
docker-compose-path: './yandex-cloud/docker-compose.yc.yaml'
version: '3.7'
services:
nginx:
image: {{ env.NGINX_IMAGE }}
ports:
- '80:80'
- '443:443'
restart: always
environment:
NGINX_CERT: {{ env.NGINX_CERT }}
NGINX_CERT_KEY: {{ env.NGINX_CERT_KEY }}
depends_on:
- frontend
- backend
frontend:
image: {{ env.FRONTEND_IMAGE }}
restart: always
backend:
image: {{ env.BACKEND_IMAGE }}
environment:
ORIGINS: {{ env.FRONTEND_ORIGINS }}
restart: always
Передача информации о пользователях
Для инициализации VM используется cloud-init. Пользователи создаются на основании конфигурации cloud-config. Например, вот конфигурация для создания единственного админа:
#cloud-config
users:
- name: {{ env.YC_VM_USERNAME }}
groups: sudo
shell: /bin/bash
sudo: [ 'ALL=(ALL) NOPASSWD:ALL' ]
ssh_authorized_keys:
- {{ env.YC_VM_SSH }}
Для небольшого приложения этого достаточно.
* Заметьте #cloud-config на первой строчке - он информирует cloud-init об использовании именно cloud-config. Его необходимо указать.
Итоговый step деплоя
update-yc:
runs-on: ubuntu-latest
needs: [build-backend, build-frontend, build-nginx]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Deploy COI VM
id: deploy-coi
uses: yc-actions/yc-coi-deploy@v1.0.1
env:
BACKEND_IMAGE: ${{ env.BACKEND_IMAGE }}
FRONTEND_IMAGE: ${{ env.FRONTEND_IMAGE }}
NGINX_IMAGE: ${{ env.NGINX_IMAGE }}
FRONTEND_ORIGINS: ${{ secrets.FRONTEND_ORIGINS }}
YC_VM_SSH: ${{ secrets.YC_VM_SSH }}
YC_VM_USERNAME: ${{ secrets.YC_VM_USERNAME }}
NGINX_CERT: ${{ secrets.NGINX_CERT }}
NGINX_CERT_KEY: ${{ secrets.NGINX_CERT_KEY }}
with:
yc-sa-json-credentials: ${{ secrets.YC_SA_JSON_CREDENTIALS }}
folder-id: ${{ secrets.YC_FOLDER_ID }}
VM-name: ${{ secrets.YC_VM_NAME }}
vm-service-account-id: ${{ secrets.YC_SERVICE_ACCOUNT_ID }}
vm-cores: 2
vm-platform-id: 'standard-v2'
vm-memory: 512Mb
vm-disk-size: 30Gb
vm-core-fraction: 5
vm-subnet-id: ${{ secrets.YC_SUBNET_ID }}
docker-compose-path: './yandex-cloud/docker-compose.yc.yaml'
user-data-path: './yandex-cloud/user-data.yaml'
Идея с сертификатом
NGINX работает на 80 порту, но хотелось бы сделать работу через HTTPS.
Как вариант, можно запускать docker-compose с 2 приложениями, а NGINX будет работать вне докера и иметь сертификат устанавливаемый вручную. Но для этого нужно подключиться по ssh и скопировать его вручную.
Я выбрал другой способ: передавать сертификат и ключ через переменные окружения при старте Docker контейнера с NGINX.
Т.к. ключ создается для сертификата с переносами строк, то и передавать NGINX`у нужно тоже файл сертификата с переносами строк. При подстановке значений в docker-compose при помощи Mustache создается невалидный yaml файл, т.к. переносы строк все ломают.
Решение костыльное - заменить все переносы строк специальным символом и передавать полученные сертификат/ключ уже в новом виде. При получении сделать обратную замену.
Заменить я решил на ;
.
Скрипт для старта NGINX:
#!/usr/bin/env sh
if [ -z "${NGINX_CERT_KEY}" ]; then
echo "NGINX_CERT_KEY env variable is not provided. Provide private key for encrypted certificate in it"
exit 1
fi
if [ -z "${NGINX_CERT}" ]; then
echo "NGINX_CERT env variable is not provided. Provided encrypted certificate in it"
fi
mkdir -p /etc/nginx/certs
echo "$NGINX_CERT" | tr ';' '\n' > /etc/nginx/certs/certificate.crt
echo "$NGINX_CERT_KEY" | tr ';' '\n' > /etc/nginx/certs/certificate.key
nginx -g 'daemon off;'
Итог
Настройка заняла примерно 2 дня. Большая часть времени ушла на фикс косяков. Например, не добавил #cloud-config в user-data.yaml или не дал права сервисному аккаунту.
Полезные сслыки:
В последней ссылке описаны экшены для деплоя Serverless Containers (деплой единственного образа) и Serverless Function.
Код проекта можно посмотреть здесь.
Готовое приложение: https://text-to-image.online