Как развернуть производственное приложение на VPS и автоматизировать процесс с помощью Docker, GitHub Actions и AWS ECR.

В этой статье мы узнаем, что означает deploying приложения в продакшн и как сделать этот процесс автоматическим, рассмотрим Docker и GitHub Actions для этого. Поскольку мы используем Docker, я не буду тратить слишком много времени на то, какие технологии использовались для написания приложения, а сосредоточусь на том, что делать с Docker-изображением. Предположение здесь означает простой сервер, пусть это будет простой VPS (Виртуальный частный сервер), выделенный сервер или любой сервер, где у вас есть SSH для использования. Мы должны выполнить действия, подготовить сервер для получения и выполнения приложения, и настроить pipeline deployments. В следующей части статьи я буду считать, что у вас есть сервер Ubuntu

Первое, что нужно сделать на сервере, это выполнить однократную настройку, цель которой – подготовить сервер для следующих шагов. Для этой цели нам нужно рассмотреть следующие темы:

Docker Compose — это инструмент, который запускает множество приложений Docker в качестве служб и позволяет им общаться друг с другом, а также обладает другими функциями, такими как тома для хранения файлов. Давайте сначала установим Docker и Docker Compose на сервер; вы можете следовать официальному руководству по установке здесь: docs.docker.com/engine/install/ubuntu. После этого важно запустить его от имени пользователя, отличного от root; вот что говорит официальная документация: docs.docker.com/engine/security/rootless.

Заметим, что вы можете достичь той же цели с использованием другого реестра, если хотите, я просто использую AWS, так как это проще для меня.

Поскольку мы используем Docker, изображения должны храниться и извлекаться откуда-то. Для этой цели я использую AWS ECR (Amazon Web Services Elastic Container Registry). Это Docker-реестр в аккаунте AWS. Он дешев в использовании и легко настраивается. Вы также можете использовать Docker Hub для создания私家ного репозитория для ваших изображений. Всё начинается с создания私家ного реестра ECR в аккаунте AWS. Вы нажмёте на “Создать репозиторий” и впишете имя репозитория.

После создания репозитория вы можете скопировать URI репозитория и сохранить его на будущее. У него следующий формат AWS_ACCOUNT_ID.dkr.ecr.AWS_REGION.amazonaws.com/REPOSITORY_NAME .

Вам также потребуется настроить учетные данные AWS IAM, которые имеют права на извлечение/推送 в этот репозиторий. Перейдите в сервис IAM, нажмите на нового пользователя и прикрепите к нему следующую политику: AmazonEC2ContainerRegistryFullAccess, вам не нужно предоставлять ему доступ к консоли AWS. В конце этого процесса вы получите 2 ключа от AWS, секретный ключ и идентификатор секретного ключа, сохраните их в сторон, они нам понадобятся дляfuture работы.

Вернувшись на наш сервер, нам нужно установить AWS CLI. Официальный способ установки доступен здесь. https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions. После этого вы можете проверить установку, запустив команду aws --version. На этом этапе вам нужно будет выполнить команду aws configure и ответить на вопросы, предоставив предыдущие ключи, созданные в AWS, секретный ключ и идентификатор секретного ключа. Вас также спросят выбрать формат вывода, просто JSON, и указать defaultCenter, лучше выбрать регион, где вы создали реестр ECR ранее.

В了我的м рабочем процессе я написал небольшой скрипт shell, который выполняет определенные действия, это ключевой элемент этого процесса, он входит в реестр, загружает изображение и перезапускает соответствующую docker-сервис, я просто называю его redeploy.sh и сохраняю его в папке, из которой я хочу запустить мое приложение, вот его содержимое:

#!/bin/bash

# Retrieve AWS ECR login command
aws ecr get-login-password --region [SWS_REGION] | docker login --username AWS --password-stdin [AWS_REGION].dkr.ecr.us-west-2.amazonaws.com

# Associating repositories with identifiers
declare -A repositories=(
    ["web"]="[REGISTRY_NAME]:latest"
)

# Check if service identifier is provided as a command line argument
if [ -z "$1" ]; then
    echo "Please provide a service identifier as a command line argument."
    exit 1
fi

service_identifier=$1

# Check if the provided service identifier exists in the repositories array
if [ -z "${repositories[$service_identifier]}" ]; then
    echo "Invalid service identifier. Available identifiers: ${!repositories[@]}"
    exit 1
fi

# pull the new image from the registry
repository=${repositories[$service_identifier]}
echo "Pulling [AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"
docker pull "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/$repository"

# Change directory to [APP_FOLDER] 
cd /home/ubuntu/[APP_FOLDER] || {
    echo "Failed to change directory to /home/ubuntu/[APP_FOLDER]"
    exit 1
}

# stop and restart the service, this wil force docker compose to redownload the lates image
echo "Re-running service $service_identifier"
docker compose stop "$service_identifier"
docker compose up --no-deps "$service_identifier" -d

# Remove old and un-used docker images
echo "Removing unused Docker images"
docker image prune -fa
echo "Removed Dangling Images"

Первый шаг этого скрипта заключается в входе в аккаунт AWS с помощью AWS CLI, чтобы получить токен, который docker будет использовать при получении docker-изображения, помните, что реестр частный, мы не можем просто забрать его, не пострадав.

Затем мы объявляем список репозиториев и связываем их с каким-то идентификатором,指定的 идентификатор будет использоваться в качестве аргумента командной строки, об этом подробнее позже. После этого мы проверяем, предоставил ли пользователь аргумент, соответствующий существующему идентификатору услуги, мы хотим, чтобы он набрал что-то вроде ./redeploy web, например, скрипт свяжет аргумент web с репозиторием web, как на第二步.

После получения идентификатора услуги мы создаем URL репозитория динамически и выполняем docker pull с ним. Это обеспечивает загрузку docker-изображения на нашу систему.

Теперь скрипт перейдет в папку приложения, /home/ubuntu/[APP_FOLDER], это предполагает, что вы все запускаете под пользователем ubuntu и что его папка HOME называется ubuntu, APP_FOLDER содержит весь набор.

Следующий шаг заключается в остановке и запуске услуги, после чего мы просто удаляем старые иunused изображения командой docker image prune -fa, вы можете узнать больше здесь: https://docs.docker.com/reference/cli/docker/system/prune/.

Compose — это утилита, которая запускает нашу целую систему, ей needed файл с именем docker-compose.yml, где вы будете определять все, предположим, наше приложение нуждается в услугах redis и postgres для работы, вот как это будет выглядеть:

version: '3.9'
services:
  web:
    image: "[AWS_ACCOUNT_ID].dkr.ecr.[AWS_REGION].amazonaws.com/myapp:latest"
    ports:
        - 8080:8080
    depends_on:
        - redis
        - db
    env_file:
        - .env

  redis:
    image: 'redis:alpine'
    ports:
      - '6379:6379'
  db:    
    image: 'postgres:14'
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      PGDATA: /var/lib/postgresql/data/pgdata
    healthcheck:
      test: [ "CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres" ]
      timeout: 45s
      interval: 10s
      retries: 10
    ports: 
      - '5437:5432'
    volumes:
      - ./opt/postgres/data:/var/lib/postgresql/data

Ваш том ./opt/postgres/data:/var/lib/postgresql/data будет отображать содержимое сервера Postgres на локальный диск, чтобы оно не было утеряно приостановке работы контейнера Docker. Узнайте больше о запуске Postgres с помощью docker-compose здесь https://medium.com/@agusmahari/docker-how-to-install-postgresql-using-docker-compose-d646c793f216. Я использовал директиву env_file, которая позволяет docker-compose читать файл и загружать его содержимое в контейнер Docker во время выполнения, я сделал это, потому что обычно файл docker-compose добавляется в VCS, и я не хочу хранить переменные окружения в нем напрямую через директиву environment в службе. Обратите внимание, что наша служба здесь называется web, ранее мы написали файл redeploy.sh и intendirium его запустить следующим образом:

 ./redeploy.sh web

Аргумент web связан с именем нашей службы, этот файл просто отображает аргумент на имя службы в docker-файле.

На этом этапе мы должны создать.service Linux, которая будет обеспечивать запуск приложения каждый раз, когда запускается сервер или наше приложение останавливается. Следующий скрипт поможет вам это сделать:

[Unit]
Description=[APP_NAME] service executed by docker compose
PartOf=docker.service
After=docker.service
After=network.target

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/home/ubuntu/[APP_FOLDER]
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down

[Install]
WantedBy=multi-user.target

Давайте проанализируем это !!!

  • Раздел Unit описывает нашу службу и specifies, частью какой службы является наш модуль, в данном случае это служба docker, это обеспечит работу нашей службы всегда, когда работает служба docker.

  • Раздел Service описывает, как запустить наш сервис, интересные части это WorkingDirectory, ExecStart и ExecStop команды, они будут использоваться в соответствии с их названием, например, если сервис называется myapp, то при вводе команды systemctl start myapp будет выполнена команда ExecStart. Вы можете узнать больше о службах Linux здесь https://www.redhat.com/sysadmin/systemd-oneshot-service. Подробнее о том, как запустить сервис docker с помощью systemd, вы можете узнать здесь: https://bootvar.com/systemd-service-for-docker-compose/

Этот сервис должен быть установлен таким образом, чтобы система запускала его по мере необходимости, вам придется сохранить его в файле с именем, например: myapp.service

touch myapp.service
# open it
nano myapp.service
# paste the previous scrip in it
cp myapp.service /etc/systemd/system/myapp.serivce

На этом этапе он признан Linux сервисом, вы можете запустить systemctl start myapp, чтобы его запустить. Следующая необходимая команда:

systemctl enable myapp.service

Это обеспечит автоматический запуск сервиса сервером при каждом перезагрузке. Вы можете узнать больше здесь: https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6

Я использовал Nginx для этой задачи, он мал и мощен, широко используется и может действовать в качестве балансировщика нагрузки, сервера статических файлов,反向 прокси и многого другого. Первое, что нужно сделать, это установить его.

sudo apt-get install nginx

На этом этапе docker-изображение якобы работает, предположим, оно содержит приложение, работающее на порту 8080, и этот порт привязан к серверу через файл docker-compose. Нам нужно настроить конфигурацию обратного прокси между Nginx и нашим портом. Вот необходимая конфигурация:

upstream app_backend {
  server localhost:8080; # the appliction port
}

server {
  listen 80;
  server_name [DOMAIN_NAME];

  location / {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
  }
}

позвольте назвать эту конфигурацию myapp.conf и сохранить ее в папке, где nginx ее найдет, эта папка среди других называется /etc/nginx/conf.d/.

sudo touch /etc/nginx/conf.d/myapp.conf
sudo nano /etc/nginx/conf.d/myapp.conf
# paste the content there

Теперь все, что нам нужно, это протестировать и перезапустить сервис NGINX с помощью следующих команд

sudo nginx -t # test if the config is valid 
sudo nginx -s reload # reload the nginx service so it will consider it

Эта конфигурация instruct nginx listened to the traffic on the port 80 and with the domain name [DOMAIN_NAME] and send it to your app server on the port 8080 via the directive proxy_pass, строка location / { просто означает перехват всех запросов, начинающихся с /, и выполнение действий, записанных под блоком location. Узнайте больше здесь https://www.theserverside.com/blog/Coffee-Talk-Java-News-Stories-and-Opinions/How-to-setup-Nginx-reverse-proxy-servers-by-example.

После настройки сервера нам нужно настроить конвейер сборки, который состоит в основном из одного шага – написать файл конвейера Github Action и добавить его в проект, приступаем.

GitHub action будет использоваться для сборки образа докера из нашего исходного кода и заталкиваться в реестр, откуда образ будет извлекаться и выполняться на сервере. Я возьму пример Dockerfile для этого примера, но на практике вам придется написать свой собственный Dockerfile. Для приложения express.js файл docker будет выглядеть так:

# Fetching the minified node image on apline linux
FROM node:slim

# Declaring env
ENV NODE_ENV production

# Setting up the work directory
WORKDIR /express-docker

# Copying all the files in our project
COPY . .

# Installing dependencies
RUN npm install

# Installing pm2 globally
RUN npm install pm2 -g

# Exposing server port
EXPOSE 8080

# Starting our application
CMD pm2 start process.yml && tail -f /dev/null

Создание и запуск этого файла docker запустит наше приложение на порту 8000, но в нашем случае мы должны запустить его с помощью docker-compose.

Далее нужно настроить конвейер действий GitHub. Для этого просто создайте папку .github/workflows в корне проекта и создайте файл с именем docker-build.yml, в него мы запишем наш конвейер.

name: Build, Push to ECS and Deploy to Server

on:
  push:
    branches: ['deploy/main']

jobs:
  build:
    name: Build Web Image
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: [AWS_REGION]

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: [REPOSITORY_NAME]
          IMAGE_TAG: latest
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Restart the service via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          port: ${{ secrets.PORT }}
          script: /home/ubntu/[APP_DIRECTORY]/redeploy.sh web

Здесь нужно выполнить несколько шагов:

  • Конфигурировать учетные данные AWS: здесь система загрузит предыдущий aws-ключ, который вы создали ранее, вам нужно будет зарегистрировать его в секретах вашего аккаунта GitHub

    .

  • Build, tag, and push image to Amazon ECR : на этом шаге будет выполнена команда docker build и docker push для создания образа docker

    .

  • Перезапуск услуги через SSH этот шаг подключится к серверу и перезапустит все приложение сразу.

Эта конвейерная линия будет запускаться каждый раз, когда.merge-запрос будет объединен с веткой deploy/main.

on:
  push:
    branches: ['deploy/main']

На этом этапе вся система настроена и связана, теперь можно редактировать и применять её к вашему конкретному случаю. В следующей статье я поделюсь процессом создания приложения для производства и запуска в docker файле.

Эта статья пытается описать процесс, который я использую для настройки VPS для автоматизации при deployment. Она описывает, как настроить процесс выполнения приложения внутри сервера и процесс создания приложения, каждую часть можно выполнить с помощью другого инструмента, например, вы можете заменить nginx на Treafik, если хотите, и можете заменить сервис systemd на программу в supervisor и более. Этот процесс не охватывает дополнительные вещи, такие как резервное копирование сервера или закрытие defaultCenter портов на серверах, об этом будет рассказано в будущих статьях. Не стесняйтесь задавать вопрос, если вы хотите адаптировать это к вашему потоку. В другой статье я сосредоточусь на том, как настроить приложение для production-ready в плане deployment, это та часть процесса, которая предшествует созданию Docker Image.

Надеюсь, вам понравилось чтение.

Я здесь, чтобы помочь вамImplementировать это в вашей компании или команде, чтобы вы могли сосредоточиться на своих основных задачах и сэкономить деньги перед тем, как начать использовать огромный потенциал Mastodon.

Source:
https://blog.adonissimo.com/how-to-deploy-a-production-app-on-a-vps-and-automate-the-process-with-docker-github-actions-and-aws-ecr