Недавно я столкнулся с ситуацией, когда мне нужно было развернуть приложения Node.js на мои собственные серверы 1 Отказ Когда я начал эту попытку, я пытался найти полезный материал, чтобы я не должен был понять все это, но все, что я мог найти, было «использовать nginx» и «возможно, использовать PM2. «Это были полезны рекомендациями, но это все еще оставило много деталей для меня, чтобы выяснить. В этом посте я обсудим проблемы, с которыми я столкнулся, и решения, которые я выбрал, чтобы, возможно, это помогает кому-то другому в будущем, который сталкивается с аналогичными проблемами.
Мы осмотрим следующие темы:
- Проверка клавишных клавиш
- Удаленно выполняет сценарий развертывания на VMS
- Управление процессами Node.js с PM2
- Синие/зеленые развертывания с nginx
- Параллельные развертывания
- Многоразовое действие частного GitHub
- Стирные секреты в журналах действий GitHub
Требования
- Развертывание нулевого простоя. Я мог бы легко оправдать управление тем, что это слишком сложно, и у нас должно быть окно обслуживания, но в эти дни ожидается развертывание нулевого простоя в наши дни, особенно для приложений для интерфейса. Для моего себя ради (мою гордость и мою совесть) я хотел сделать это.
- Автоматически развертываю всякий раз, когда главная ветвь обновляется. Я не знаю, насколько это обычно Но я делал это годами с Героку И я не могу представить какой-либо другой способ развития. Случай вручную развертывание чувствует архаичную.
- Развертывание в существующих машинах. Цели развертывания были бы набором производства VMS, которые в настоящее время используются. У меня не было возможности использовать новые виртуальные машины и заменять старые.
Реализация
Мы уже использовали действия GitHub, чтобы запустить тесты против всех PR, поэтому я подумал, что мы также использовали их, чтобы вызвать развертывание, когда главная ветка обновляется.
Концептуально я представлял, что процесс будет выглядеть что-то подобное:
- Толчок к мастеру вызывает развертывание
- Подключитесь ко всем развертыванию целей (серверов) и запустить скрипт, который устанавливает и запускает новый код
- Отвлекайте трафик от старого кода к новому коду
- Уборка старого кода
Мне потребовалось 3-4 дня, чтобы получить от этого высокоуровневой очереди до окончательной реализации. Я объясню, где я закончил и почему я сделал определенный выбор.
Проверка клавишных клавиш
Одной из первых проблем, которые я столкнулся с ключевыми клавишами. Когда вы сначала SSH в машину, запрос спрашивает вас, доверяете ли вы ключу удаленного сервера. Но я управлял это в сценарии, поэтому мне нужно было избежать этого подсказки. Вы можете отключить его, но это считается опасным из-за потенциальных атак на человека в среднем. Альтернатива — использовать SSH-KEYSCAN
Чтобы автоматически добавить удаленные клавиши в свой доверенный список.
ssh-keyscan "$IP" >> ~/.ssh/known_hosts
Но я не вижу, как это все более безопасно. В любом случае, вы слепо доверяете IP. Какие альтернативы? Возможно, вы могли бы вручную запустить SSH-KEYSCAN
один раз для каждого хоста, а затем сохранить результат в конфигурации, которые затем добавляются в известные_хосты
Отказ
Удаленно выполняет сценарий развертывания на VMS
У меня был список IPS, которые были развертываемыми целями и ключом SSH. Как-то мне нужно было запустить набор команд на VMS, который фактически выполнил бы развертывание. Набор команд начался маленький Так что я начал с помощью Appleboy/SSH-Action Отказ
- name: SSH Commands uses: appleboy/ssh-action@v0.1.3 env: GH_TOKEN: ${{ secrets.GH_TOKEN }} with: host: ${{ secrets.DEPLOY_IP }} username: ${{ secrets.DEPLOY_USERNAME }} key: ${{ secrets.SSH_KEY }} script_stop: true envs: GH_TOKEN script: | cd /srv/bg git clone --depth 1 "https://${GH_TOKEN}@github.com/Org/Repo.git" cd bg-web npm i npm run build npm run start
Но мой короткий список команд быстро рос, и я скоро желал поддерживать сценарий Bash, который будет выполнен удаленно. Поэтому я переключился на что-то подобное:
- name: Deploy run: | KEY_FILE=$(mktemp) echo "${{ secrets.SSH_KEY }}" > "$KEY_FILE" ssh -i $KEY_FILE ubuntu@${{ secrets.DEPLOY_IP }} -- < deploy.sh
Это работало хорошо. Мне особенно понравилось, имея синтаксис подсветки при работе над сценарием развертывания. Но в конечном итоге я хотел больше, например, в результате регистрации сценариев развертывания в файл временного журнала и передачи ENV VARS в сценарий. Я решил просто скопировать сценарий развертывания на VM перед выполнением. У меня уже была доступна ключ SSH, которая сделала это легко с SCP:
# Transfer the deploy script onto the VM so that we can execute it later. # If we have previously deployed to the VM, an older version of the script will be there and be overwritten with the latest version. scp -i $KEY_FILE /scripts/deploy.sh ubuntu@$IP:~/ # Execute the deploy script and save the logs to a temp file. ssh -i $KEY_FILE ubuntu@$IP "tmpfile=$(mktemp /tmp/deploy.XXXX); echo \"Deploy log for $IP saved in \$tmpfile\"; GH_TOKEN=$GH_TOKEN IP=$IP REPO=$REPO bash deploy.sh > \$tmpfile 2>&1"
Это то, что я закончил. Единственное, что мне не нравится в этом, — это список переменных среды (список на самом деле намного дольше в версии, которую я использую). Если вы знаете лучше, пожалуйста, дайте мне знать.
Управление процессами Node.js с PM2
Node.js — это однопоточная, что означает, что вам необходимо запустить несколько экземпляров того же процесса, чтобы использовать все доступные сердечники CPU. Как правило, это делается с Кластер API . Я использовал это раньше И я не хотел использовать его снова. Вы должны настроить главный файл, который порождает процессы и управляет своим жизненным циклом, обрабатывает ошибки, образует процессы, которые умирают и т. Д. Вместо того, чтобы обработать все это, я решил использовать PM2. . Теперь кластеризация приложения так же просто, как:
pm2 start -i max --name $PROCESS_NAME $START_COMMAND
Позже, когда мне нужно очистить старый код, я могу использовать PM2 Список
Чтобы найти какие-либо процессы, которые не соответствуют новым $ Process_name
и убить их с PM2 Удалить
Отказ Больше на этом в следующем разделе.
Синие/зеленые развертывания
А синее/зеленое развертывание Один из способов добиться развертывания нулевого простоя, вращая новый сервер, затем маршрутизирующую трафик до него перед выходом на отставку старого сервера. Тем не менее, у меня не было предоставления нового сервера, поэтому я должен был достичь то же самое на существующем сервере.
Трафик вошел на порт 80 или 443. Привязка к этим портам требует корневых привилегий. Но вы не хотите, чтобы ваше веб-приложение иметь права корневых привилегий. Так что вы можете либо использовать iptables. Чтобы перенаправить порт 80 в ваше приложение, или вы можете использовать Nginx. Мы выбрали Nginx, потому что он предлагает гораздо больше на пути конфигурации HTTP, которую мы ожидаем, нуждающимся в будущем (SSL-сертификаты, заголовки и т. Д.).
Мы начинаем с файла Conf в /etc/nginx/на сайте
Это выглядит так:
server { listen 80; server_name domain.com; location / { proxy_pass http://localhost:3000; } }
Позже, когда мы развертываем новый скрипт, порт 3000 уже используется, поэтому нам нужно использовать другой порт. Мы могли бы постоянно поменяться взад-вперед между портом 3000 и 3001, но отслеживание того, какой порт в настоящее время требуется государства и чувствует себя хрупким. Поэтому я решил случайным образом генерировать порт каждый раз, затем проверяя, что в данный момент он не используется.
# Picks a random number between 3000 and 3999. function random-number { floor=3000 range=3999 number=0 while [ "$number" -le $floor ] do number=$RANDOM let "number %= $range" done echo $number } # Pick a random port between 3000 and 3999 that isn't currently being used. PORT=$(random-number) while [[ $(lsof -i -P -n | grep :$PORT) ]] do PORT=$(random-number) done echo "Ready to deploy on port $PORT"
Я также использовал номер порта в каталоге, где я установил код (чтобы убедиться, что не было никаких конфликтов с предыдущими установками) и идентифицировать процессы с регистрацией их с PM2.
Теперь мы обновляем Nginx Conf:
sudo cat << EOF | sudo tee /etc/nginx/sites-enabled/site.conf > /dev/null server { listen 80; server_name domain.com; location / { proxy_pass http://localhost:$PORT; } } EOF
Хотя файл конфигурации изменился, Nginx еще не в курсе. Мы можем сказать ему перезагрузить файл, отправив сигнал перезагрузки:
sudo nginx -s reload
Nginx Docs. Скажи, что это должно произойти изящно:
Он начинает новые рабочие процессы и отправляет сообщения на процессы старых работников, запрашивая их изящно отключаться. Старые работники процессы закрывают розетки и продолжают обслуживать старых клиентов. Ведь все клиенты обслуживаются, старые рабочие процессы закрыты.
Это прекрасно. Требуется изящно передавать трафик, чтобы мы не должны были. Тем не менее, это не издает сигнал, когда передача сделана. Итак, как мы узнаем, когда мы можем уйти в отставку и очистить старый код?
Один из способов наблюдать за движением к вашим процессам. Но это звучит сложно для меня. Есть несколько процессов. Откуда я знаю, когда трафик готов к ним поехать на все? Если у вас есть какие-либо идеи, я бы слышал. Но я пошел с другим решением.
Я понял, что имело фиксированное количество рабочих процессов (что, по-видимому, привязано к количеству процессовых сердечников). Но этот пункт, который я цитировал выше о перегреве, говорит, что он начинает новые работники параллельно старым, поэтому во время перезагрузки у вас есть 2x количество работников. Поэтому я понял, что я могу рассчитывать на количество рабочих процессов до перезагрузки, а затем подождать, пока количество работников не вернутся к нормальному. Это сработало.
function nginx-workers { echo $(ps -ef | grep "nginx: worker process" | grep -v grep | wc -l) } # Reload (instead of restart) should keep traffic going and gracefully transfer # between the old server and the new server. # http://nginx.org/en/docs/beginners_guide.html#control echo "Reloading nginx..." numWorkerProcesses=$(nginx-workers) sudo nginx -s reload # Wait for the old nginx workers to be retired before we kill the old server. while [ $(nginx-workers) -ne $numWorkerProcesses ] do sleep 1; done; # Ready to retire the old code
Это не на 100% нулевое время. Я сделал нагрузочное тестирование, чтобы подтвердить, что есть около секунды времени. Я не знаю, это потому, что я все еще убиваю старые процессы слишком рано или если это потому, что nginx отказывается отключение соединений. Я пытался добавить больше Спать
После цикла, чтобы убедиться, что все соединения слились и прекращены, но вообще не помогло. Я также заметил, что ошибки (во время теста нагрузки) были о том, что не смогли установить соединение (в отличие от того, что соединение раннется рано), что приводит меня к верить, что это из-за перезарядки Nginx, не являющихся 100% изящными. Но это все достаточно хорошо на данный момент.
Теперь мы готовы очистить старый код:
# Delete old processes from PM2. We're assuming that traffic has ceased to the # old server at this point. # These commands get the list of existing processes, pair it down to a unique # list of processes, and then delete all but the new one. pm2 list | grep -o -P "$PROCESS_NAME-\d+" | uniq | while IFS=$'\n' read process; do if [[ $process != $PROCESS_NAME-*$PORT ]]; then pm2 delete $process fi done # Delete old files from the server. The only directory that needs to remain # is the new directory for the new server. So we loop through a list of all # directories in the deploy location (currently /srv/bg) and delete all # except for the new one. echo "Deleting old directories..." for olddir in $(ls -d /srv/bg/*); do if [[ $olddir != /srv/bg/$PORT ]]; then echo "Deleting $olddir" rm -rf $olddir else echo "Saving $olddir" fi done;
Параллельные развертывания
Сначала я получил синее/зеленое развертывание на одной машине. Я подумал, что было бы легко изменить так, чтобы он работает на нескольких машинах, зацикливаясь через список IP-адресов. Вероятно, было бы легко, если бы я сделал развертывание серийно, но я хотел сделать развертывание параллельно, чтобы уменьшить время, проведенное на развертывании. Я надеялся, что смогу просто фоном команду ssh SSH &
Отказ Но я получил некоторое сообщение об ошибке о том, как это было неверно. Поиск в Интернете выявлено множество альтернатив Это не сработало, иначе не было легко обеспечить идентификатор ребенка (более позже, почему нам это нужно). Наконец я закончил просто создать другой скрипт Bash, который имел команды SCP и SSH. Тогда я мог бы легко предпосылл выполнение этого сценария Bash.
# Turn the list of IPs into an array IPS=( $DEPLOY_IPS ) for IP in "${IPS[@]}"; do echo "Preparing to connect to $IP" # Here's that list of env vars again KEY_FILE=$KEY_FILE GH_TOKEN=$GH_TOKEN IP=$IP REPO=$GITHUB_REPOSITORY bash /scripts/connect.sh & done
Поэтому я оказался этим трио сценариев:
deploy-manager.sh -> connect.sh -> deploy.sh
Но как мне знать, когда развертывается и как я узнаю, не удается ли один из них? Я нашел Хорошее решение На сайте Unix & Linux Stockexchange. Вы просто собираете идентификаторы дочерних процессов, а затем подождите всех из них, чтобы убедиться, что их коды выхода 0.
Что вы делаете, если развертывание не удалось на одном компьютере, но удастся на другой? Я еще не решил эту проблему. Есть идеи?
Многоразовое действие частного GitHub
После того, как я получил все это, работаю в одном репо, с несколькими развертывающими целями, я решил переместить его в частное действие GitHub, чтобы его можно было поделиться несколькими приложениями Node.js. Я ожидал, что это будет легко, потому что у меня уже был весь рабочий код. Но как всегда я ошибился.
Во-первых, GitHub официально не поддерживает частные действия, но вы можете обойти его с Удобное решение Отказ
GitHub предлагает два варианта реализации для пользовательских действий: Node.js или Докер Отказ Я написал действия Node.js раньше, и мне не наслаждались опытом столько, сколько я надеялся. Требуется, чтобы вы совершали подключенный код для вашего репо, потому что он не устанавливает зависимости для вас. Вы, вероятно, можете уйти без использования DEPS, если вы усердно работаете, но еще более неудобно не использовать @ Действия/ядро . Это также неправильно написать сценарий узла, который только что выполняет сценарий Bash. Поэтому я решил создать докер действий.
Я предположил, что все, что мне нужно было ванильное докер, которое бы выполнить Deploy-Manager.sh
скрипт Но я быстро побежал в проблемы. Мои сценарии были разработаны для выполнения на рабочих процессах GitHub. Я указал Ubuntu-Neighle и предполагал, что это красивая ванильная установка. Но оказывается они Установите тонны программного обеспечения и к сожалению не имейте в наличии докерный контейнер Отказ К счастью, все, что мне нужно было установить, было openssh-сервер
Отказ Вот мой последний докерфайл:
FROM ubuntu:18.04 RUN apt update && apt install -y openssh-server COPY scripts/*.sh /scripts/ ENTRYPOINT ["/scripts/deploy-manager.sh"]
Я столкнулся с другой проблемой. Проверка ключа хоста начала не удалять, когда я переключился на действие докера. Это потому, что Действия Docker GitHub являются Беги как root Пока я разработал скрипты, работающие как пользователь Ubuntu. У пользователей есть свои известные_хосты
Файл расположен в ~/.ssh/seval_hosts
Отказ Но для корня мне нужно было изменить глобальный файл, расположенный в /etc/ssh/ssh_known_hosts
Отказ
Я был рад учиться Докера, но я мог бы переоценить решение использовать его. Лучше лучше построить контейнер каждый раз, когда действие работает или для совершения подключенного кода к вашему действию REPO? 😬
Стирные секреты в журналах действий GitHub
Если вы хотите иметь пользовательские переменные среды в рабочих процессах GitHub, ваш единственный вариант должен использовать Секреты Отказ Один из моих секретов хранит список IPS для развертывания целей. Но это не совсем что-то, что мне нужно поддерживать частное и часто полезно в логах отладки.
Github Scrubs Действия журналы для автоматического Redact Secrets. Поскольку мои IPS были в списке, и я был только печатать, я понял, что не будет отредактирован. Но это было! Они должны делать частичные сопоставления на секретах (мне интересно, какую длину персонажей они используют). Чтобы обойти это, я использовал $ Unsect_ip
Переменная, которая была $ IP
Со всеми точками заменены тиреми. Конечно, это не было отредактировано.
UNSECRET_IP=$(echo $IP | tr . -)
Заключение
Это много работы, и он даже не обрабатывает сбои частичного развертывания, спирали или управление журналом. Я представляю, что я потрачу немного времени, поддерживая это творение. Это цементируется верой в ценность поставщиков PAAS. Я бы предпочел платить кому-то, чтобы сделать это для меня и сделать это намного лучше, чем я могу.
Я предпочитаю использовать поставщики PAAS, как Heroku, NetLify, и Vercel, чтобы я не должен делать все обсуждаемое здесь 😂. ↩
Оригинал: «https://dev.to/justincy/blue-green-node-js-deploys-with-nginx-bkc»