В прошлой статье мы рассмотрели чрезвычайно популярный инструмент для выкатки приложений Jenkins. Мы подружили его через плагины с SSH, с GitHub, построили простой пайплайн с помощью Groovy. И вроде все здорово, все работает как должно, но все равно есть ощущение, что можно сделать лучше. И действительно, наш процесс можно улучшить, перестав проводить сборку на VPS.

Ранее для сборки мы использовали агент Jenkins, который был установлен на нашем хостинге, где и происходила сборка приложения и его выкатка. Конечно, в реальных проектах существует больше одного боевого сервера, существуют промежуточные серверы – тестовые, демо, стейдж. Не всегда и везде, конечно, но когда у вас несколько серверов, то и сборку приложений можно проводить на каких-то промежуточных, а после всех тестов и проверок, рабочее собранное приложение доставлять до прода. Но у нас все по простому, сразу в прод и агент был там же.

Источник изображения: Medium
Источник изображения: Medium

В этой статье предлагаю воспользоваться альтернативным подходом – собирать наше приложение, не покидая репозиторий, никаких дополнительных программ, и никаких агентов. Одним из таких инструментов является GithubActions, который позволяет создавать пайплайн деплоя и настроить доставку приложения на хостинг различными способами.

Для начала конечно стоит изучить документацию и рассмотреть приведенные примеры, посмотреть раздел быстрого старта.

Из него становится ясно, что для создания собственного процесса нам нужно создать конфигурационный файл с расширением .yml внутри нашего репозитория в каталоге .github/workflows . Что указывать в этом файле? Это подробно рассмотрено также в документации, в соответствующем разделе.

На самом деле, если язык Groovy, используемый в Jenkins может вызывать сложность в понимании, то здесь используется YAML, и если вы хоть раз составляли какие-то конфигурационные файлы или хотя бы делали docker-compose, то у Вас не должно возникнуть с ним никаких трудностей. Поэтому, дабы не распыляться в пустую, перейдем к сути.

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

  • сборка приложения и подготовка артефакта;

  • перенос артефакт на хостинг;

  • правила деплоя доставленного артефакта.

И перед тем, как мы приступим к созданию собственного сценария, нужно уделить внимание ключам. Мы уже создавали ключ в прошлой статье и размещали его в панели Jenkins, чтобы была связь с агентом, а также достаточно прав для запуска приложения. Чтобы безопасно хранить чувствительные данные, в GitHubActions есть специальный раздел настроек в репозитории «Actions secrets and variables». Подробнее можно изучить в документации, но простыми словами – это обычное хранилище ключ-значение, где мы добавляем в хранилище какое-либо значение, а затем можем обратиться к нему по ключу.

Добавили 3 ключа, которые пригодятся в дальнейшем - адрес хостинга, ключ и пользователь
Добавили 3 ключа, которые пригодятся в дальнейшем - адрес хостинга, ключ и пользователь

Начнем с общей части, описательной:

name: SimpleApp Deploy

on:
  push:
  branches: [ "master" ]

Мы создаем правило – начать деплой, если в ветку master произошел push. Одно простое правило, которое будет являться триггером. Если проект разрастается и в ветке “master” не всегда рабочая версия приложения (или не всегда релизная), то можно добавить дополнительное условие. Например, проверить содержит ли коммит значение [release].

jobs:
  deploy:
    if: contains(github.event.head_commit.message, '[release]')

Можно завезти отдельные теги c версиями и проверять их.

on:
  push:
    tags:
      - v1.**

В общем, вариантов масса в зависимости от Ваших потребностей. Разные варианты и используемые команды можно изучить в документации про триггеры и события. Мы остановимся на самом простом, будем считать триггером обновление основной ветки master. Далее настраиваем необходимые шаги.

❯ Шаг 1. Сборка приложения

Тут все как и прежде – создание docker образа. Единственное, что добавляется, это упаковка его в tar архив.

- uses: actions/checkout@v4
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Build Docker image
      run: docker build -t simpleapp .
    
    - name: Save Docker image as tarball
      run: docker save simpleapp -o simpleapp.tar

    - name: Verify tarball exists
      run: ls -lh simpleapp.tar
      
    - name: Change file permissions
      run: chmod 644 simpleapp.tar

❯ Шаг 2. Упакованный архив копируем на хостинг

- name: SSH to VPS and remove existing tarball
      uses: appleboy/ssh-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        script: |
          if [ -f /home/${{ secrets.VPS_USER }}/simpleapp.tar ]; then
            rm /home/${{ secrets.VPS_USER }}/simpleapp.tar
          fi

    - name: Copy Docker image to VPS
      uses: appleboy/scp-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        source: simpleapp.tar
        target: /home/${{ secrets.VPS_USER }}/

❯ Шаг 3. Подключаемся по ssh к хостингу и разворачиваем наше приложение с необходимым параметрами

- name: SSH to VPS and deploy
      uses: appleboy/ssh-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        script: |
          docker load -i /home/${{ secrets.VPS_USER }}/simpleapp.tar
          if [ "$(docker ps -q -f name=simpleapp)" ]; then
            docker stop simpleapp
          fi
          if [ "$(docker ps -a -q -f name=simpleapp)" ]; then
            docker rm simpleapp
          fi
          docker run --name simpleapp -p 5144:8080 -d simpleapp

Сохраняем все в файл и пушим в наш репозиторий, тем самым вызывая триггер.

Успешный деплой
Успешный деплой
Полная версия файла
name: Docker Image CI

on:
  push:
    branches: [ "master" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Build Docker image
      run: docker build -t simpleapp .
    
    - name: Save Docker image as tarball
      run: docker save simpleapp -o simpleapp.tar

    - name: Verify tarball exists
      run: ls -lh simpleapp.tar
      
    - name: Change file permissions
      run: chmod 644 simpleapp.tar
      
    - name: SSH to VPS and remove existing tarball
      uses: appleboy/ssh-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        script: |
          if [ -f /home/${{ secrets.VPS_USER }}/simpleapp.tar ]; then
            rm /home/${{ secrets.VPS_USER }}/simpleapp.tar
          fi

    - name: Copy Docker image to VPS
      uses: appleboy/scp-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        source: simpleapp.tar
        target: /home/${{ secrets.VPS_USER }}/

    - name: SSH to VPS and deploy
      uses: appleboy/ssh-action@v0.1.7
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_KEY }}
        script: |
          docker load -i /home/${{ secrets.VPS_USER }}/simpleapp.tar
          if [ "$(docker ps -q -f name=simpleapp)" ]; then
            docker stop simpleapp
          fi
          if [ "$(docker ps -a -q -f name=simpleapp)" ]; then
            docker rm simpleapp
          fi
          docker run --name simpleapp -p 5144:8080 -d simpleapp

Давайте пройдемся немного подробнее по каждому шагу, чтобы было понимание. Вначале мы используем actions/checkout, который предоставляет доступ в репозиторий. В случае наличии изменений в репозитории, переходим на следующий шаг – сборка контейнера, самая обычная с помощью команды docker build -t simpleapp .
После этого упакуем образ в архив .tar и даем права, и переходим к следующему этапу, копированию. Тут мы выполняем ровно два действия. Используя SSH, сначала проверяем, имеется ли уже в нашей директории файл с таким названием. Если имеется, то удаляем, так как следом, используя  scp-action, мы копируем наш архив на хостинг и переходим к следующему этапу.

Здесь также без каких либо сложностей. Используя ключи из секретов, мы снова подключаемся к VPS и загружаем наш образ в архиве. Скриптом проверяем, есть ли одноименный контейнер, и если есть, то останавливаем его и удаляем. И потом запускаем наш свежий командой docker run --name simpleapp -p 5144:8080 -d simpleapp

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

У такого подхода есть одно преимущество (особенно в текущих реалиях), и оно же является недостатком. Более популярным решением является использовать какое-то внешнее хранилище, куда обычно размещают собранный образ. На хостинг явно ничего не копируется, а лишь указывается – сходи в хранилище и возьми нужный образ, и разверни из него контейнер. Кто-то для этого использует Docker Hub, многие используют хранилище GitHub. Стоит отметить, что у них сильно ограничены возможности для бесплатного тарифа, это раз. Во-вторых, с их выкрутасами и выкрутасами не РКН, все это в любой момент может отвалиться и лично я предпочитаю, в текущей обстановке, по минимуму  завязываться на них, а посему использую самое просто копирование.

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

Недостаточно прав в токене, полученном при входе в ghcr.io
Недостаточно прав в токене, полученном при входе в ghcr.io

Поэтому идем сначала в раздел с токенами и создаем токен с достаточными правами на запись write:packages и read:packages, repo (если у Вас уже есть токен с такими правами, то можете его использовать), и добавляем его в хранилище секретов, как это делали в самом начале. Я добавил токен с именем GHCR_PAT. В итоге, файл приобрел следующий вид:

Полная версия файла
name: Docker Image CI

on:
  push:
    branches: [ "master" ]

jobs:

  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to GitHub Container Registry
        run: |
          echo ${{ secrets.GHCR_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

      - name: Build Docker image
        run: docker build -t ghcr.io/${{ github.repository_owner }}/simpleapp:latest .

      - name: Push Docker image to GHCR
        run: docker push ghcr.io/${{ github.repository_owner }}/simpleapp:latest

      - name: SSH to VPS and deploy
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.VPS_HOST }}
          username: ${{ secrets.VPS_USER }}
          key: ${{ secrets.VPS_KEY }}
          script: |
            echo ${{ secrets.GHCR_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
            docker pull ghcr.io/${{ github.repository_owner }}/simpleapp:latest
            if [ "$(docker ps -q -f name=simpleapp)" ]; then
              docker stop simpleapp
            fi
            if [ "$(docker ps -a -q -f name=simpleapp)" ]; then
              docker rm simpleapp
            fi
            docker run --name simpleapp -p 5144:8080 -d ghcr.io/${{ github.repository_owner }}/simpleapp:latest

Результат такой же.

Больше никаких архивов и копирований, только хранилище, только хардкор.
Больше никаких архивов и копирований, только хранилище, только хардкор.

На текущий момент мы рассмотрели самые популярные возможности деплоя наших .NET приложений, которых должно быть достаточно для запуска наших простых проектов. Несмотря на то, что наше приложение ничего из себя не представляет, мы упустили два важных момента, каждый из которых стоит рассмотреть в отдельности. Во-первых, в 2024 запускать приложение без https – это моветон. Поэтому в следующей статье будут рассмотрены различные способы получения и использования сертификатов для повышения безопасности приложения. Во-вторых, думаю стоит уделить внимание таким вещам, как метрики и логирование. Ведь мало толку запустить приложение, надо иметь возможность отслеживать, что с ним происходит, какие запросы приходят, что в этот момент делает приложение, сколько потребляет ресурсов и т.д. Думаю, что этому аспекту тоже стоит уделить пристальное внимание, если вы планируете научиться разворачивать приложения, приближенные к реальным корпоративным решениям.


Надеюсь, что данная статья помогла Вам разобраться в вопросе использования GitHub Actions для развертывания своих приложений. Если остались вопросы – можете задать в комментариях, сообщество быстро подскажет, как делать правильнее :)
Можете также подписаться на мой телеграм, чтобы быть в курсе планов выхода следующих статей.

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud  в нашем Telegram-канале 

Перейти ↩

? Читайте также:

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


  1. vitaly_il1
    02.09.2024 15:35

    1) поправьте форматтинг кода

    2) chmod и rm излишни


  1. itmind
    02.09.2024 15:35
    +1

    Docker поддерживает в контекстах удаленные инстансы через SSH. Можно еще так делать деплой контейнера:

    - name: Install ssh keys and docker remote
      run: |
         install -m 600 -D /dev/null ~/.ssh/id_rsa
         echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
         ssh-keyscan -H ${{ vars.SSH_HOST }} > ~/.ssh/known_hosts
         docker context create remote --docker host=ssh://${{ secrets.SSH_USER }}@${{ vars.SSH_HOST }}
         docker context use remote

    и потом просто вызываем команды docker и они выполняются на удаленном сервере.