Привет, Habr!

Heroku меньше чем через месяц станет недоступен для бесплатного использования. У многих моих знакомых, и у меня в том числе, на нем хостились несколько проектов. Поэтому встал вопрос миграции в другое облако. Остановился на Яндекс Облаке:

  • Относительно невысокие цены.

  • Возможность управлять как из панели, так и через CLI.

  • Интеграция с GitHub Actions.

  • Пробный период.

Задача

Есть 2 приложения: frontend и backend. Нужно выложить их на хостинг вместе. А чтобы работали вместе, склеить их с помощью nginx.

Общий вид пайплайна

  1. Пуш изменений в main.

  2. Создание Docker образов.

  3. Пуш образов в Docker Hub.

  4. Уведомление 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

Комментарии (0)