Как настроить Continuous Deployment для своего проекта: личный опыт



    Перевели для вас статью Юлиуса Минмо о настройке непрерывного развертывания (Continuous Deployment) для своего проекта. Автоматизация позволяет сэкономить кучу времени и сил. Статья будет полезна, в первую очередь, начинающим программистам.

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

    Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

    Skillbox рекомендует: Онлайн-курс «Профессия Frontend-разработчик».
    Итак, для начала давайте посмотрим на схему, где объясняется разница между Continuous Delivery и Continuous Deployment.



    В случае с домашним проектом выбираем Continuous Deployment, поскольку никто, кроме вас, с ним (проектом) не работает и никто от него не зависит. Ну а поскольку в большинстве случаев хочется, чтобы изменения были немедленно развернуты, то выбор очевиден. Если же вам позже захочется изменить процесс, вы всегда сможете это сделать.

    Вы изучите следующее:

    • Как сделать Dockerfile.
    • Как выгрузить проект на GitHub.
    • Как автоматически построить образ docker на Docker Hub.
    • Как автоматически загрузить и запустить образ с Watchtower.

    Что требуется:

    • Базовое понимание того, что представляют собой Docker и Dockerfile.
    • Установленный Git.
    • Учетная запись на <a href='https://hub.docker.com/">Docker Hub

    Сервер (физический или виртуальный) с запущенным Docker.

    Вот мои репозиторий GitHub и Docker Hub, с которыми я работаю.

    Почему я использую Docker?


    Он дает возможность использовать одно и то же окружение для разных процессов, что исключает появление гейзенбагов и проблемы «это работает только на моей машине». Контейнеры изолированы, что хорошо с точки зрения кибербезопасности. Есть и больше преимуществ, но, на мой взгляд, эти два являются главными.

    Настройка Dockerfile

    Сначала нам нужен Dockerfile для проекта. Этот файл всегда называется именно так и не имеет расширения. Он всегда находится в главной директории проекта.

    Он начинается с оператора FROM, который сообщает Docker, с какого базового образа мы начинаем. Вы можете провести аналогию с живописью. Можем представить себе этот образ как готовый холст с нарисованным фоном и отсутствующим главным элементом композиции (вашей программой).

    Далее копируем файлы проекта в контейнер при помощи команды COPY…

    Она позволяет забрать файлы из начального расположения в текущее — конечно, внутри контейнера.

    Далее необходимо установить зависимости, для этого я использую Python PIP. Главное, что нужно запомнить, — это запуск команд в контейнере с RUN.

    From python:3.7
    COPY..
    RUN pip install -r requirements.txt


    Все просто, правда? Теперь можно запускать программу в контейнере.

    CMD [«python», "./my_script.py"]

    Теперь все, вы закончили Dockerfile и можете вручную создать образ и контейнер. Сейчас просто пропустим этот момент.

    Теперь давайте создадим репозиторий в GitHub, но помните, что строку “Initialize this repository with a README” не нужно трогать.



    Теперь копируем URL.



    Открываем cmd/shell корневой директории проекта. Теперь необходимо инициализировать репозиторий, добавить файлы, сконфигурировать remote-режим, закоммитить файлы и отправить проект на GitHub.

    git init
    git add *
    git remote add origin https://github.com/<user>/<repository>.git
    git commit -a -m "Make Dockerfile ready for CD"
    git push -u origin master

    Если все ОК, GitHub-репозиторий будет выглядеть вот так:



    Мы на полпути к успеху!

    Теперь нужно подключить GitHub к Docker Hub. Для этого нужно отправиться в настройки учетной записи.



    Скролим вниз и подключаемся.



    Теперь создаем репозиторий в Docker Hub.



    Называем свой репо и кликаем по иконке GitHub или Bitbucket. Потом выбираем организацию (обычно это ваш ник) и название проекта. При желании настройки можно изменить.



    Ну а теперь последний шаг — здесь нам необходим Watchtower на целевой машине. Это программа, которая позволяет автоматизировать процесс. Если появляется апдейт, то Watchtower убирает оригинальный контейнер и создает контейнер из нового образа с такими же настройками.

    Хорошая новость в том, что можно установить Watchtower с Docker, для этого необходимо ввести в терминал такую команду:

    docker run -d --name watchtower -v /var/run/docker.sock:/var/run/docker.sock v2tec/watchtower

    И теперь запускаем контейнер для своего проекта!

    docker run -d --name <my-project> <username>/<my-project>

    -d позволяет программе работать в фоне, так что она не закроется, если вы закроете терминал.

    Завершая сказанное, если вы отправите коммит к репозиторию GitHub, Docker Hub автоматически создаст образ Docker. Затем с ним уже будет взаимодействовать Watchtower.

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

    Skillbox рекомендует:

    • +21
    • 12k
    • 8
    Skillbox
    360,00
    Онлайн-университет профессий будущего
    Поделиться публикацией

    Комментарии 8

      +7
      Режет глаз, простите, Continuous Deployment это непрерывное развертывание, а не интеграция.
        0
        Я соглашусь, что «Continuous Delivery и Continuous Deployment» в данной статье как-то кривовато объясняются. И вообще это вопрос дичайших споров, что есть что. Кратко общее мнение состоит в том, что CI — это постоянные сборки + автотесты (что и есть интеграция). Deployment — это именно развертывание постоянное на прод. А Delivery — это весь процесс поставки ценности ( т.е. CI + CD).
        Дополнительно могу добавить, что continuous не подразумевает деления между авто-развертываниями с роллбеком и ручными. Какая разница, в конце-концов? Процесс он гораздо выше уровнем этих деталей.
        0
        del
          +1
          А auto pull по вебхуку? При обновлении репозитория на гитхабе, дёргается скрипт на целевой машине, и забираются изменения. Вроде проще же, или я не прочувствовал суть улучшения?

          auto_pull.php
          <?php
          // USE APPLICATION/JSON CONTENT TYPE
          
          // https://***/auto_pull.php
          // REMOTE_REPOSITORY = 'https://github.com/***.git'
          
          define('SECRET', '***');
          define('ROOT', "$_SERVER[DOCUMENT_ROOT]/***/");
          define('BRANCH', 'refs/heads/master');
          define('LOGFILE', "$_SERVER[DOCUMENT_ROOT]/***/auto_pull.log");
          define('TIME', time());
          
          
          // log the time
          to_log(
          	"Date: " . date("Y-m-d H:i:s", TIME) . "UTC \n" .
          	"IP: " . htmlspecialchars($_SERVER['REMOTE_ADDR']) . "\n" .
          	"UA: " . htmlspecialchars($_SERVER['HTTP_USER_AGENT'])
          );
          
          
          // Get the POST-data like this: {"key1":"value1","key2":"value2","key3":"value3"}
          $payload = file_get_contents("php://input");
          
          // Payload must be non-empty
          if (empty($payload)) {
          	stop("Payload: empty. Nothing to do."); // Exit
          }
          
          
          
          // Retrieve a signature
          if (isset($_SERVER["HTTP_X_HUB_SIGNATURE"])) {	
          	$parts = explode("=", htmlspecialchars($_SERVER["HTTP_X_HUB_SIGNATURE"]), 2); // разбить на максимум 2 части по символу "="
          	
          	$algorithm = $parts[0];
          	$signature = $parts[1];
          	
          	to_log("Signature: found. Algorithm: '$algorithm'. Value: '***'"); // Log and continue
          }
          else {
          	stop("No signature found"); // Exit
          }
          
          
          
          
          // Check for a GitHub signature (hash_hmac - генерация хеш-кода на основе ключа, используя метод HMAC)
          if ($signature == hash_hmac($algorithm, $payload, SECRET)) {
          	to_log("Signature: valid"); // Log and continue
          }
          else {
          	stop("X-Hub-Signature does not match SECRET"); // Exit
          }
          
          
          
          // Is PING received? - log and exit
          if (isset($_SERVER['HTTP_X_GITHUB_EVENT']) && $_SERVER['HTTP_X_GITHUB_EVENT'] == 'ping') {
          	to_log('Event: Ping. Nothing to do');
          	ok();
          	exit();	
          }
          
          
          
          // Is PUSH received
          if (isset($_SERVER['HTTP_X_GITHUB_EVENT']) && $_SERVER['HTTP_X_GITHUB_EVENT'] == 'push') {
          	to_log('Event: push');
          
          	// Decode JSON data from Github
          	if (isset($_SERVER['CONTENT_TYPE'])) {
          		switch (htmlspecialchars($_SERVER['CONTENT_TYPE'])) {
          			case 'application/json':
          				to_log('Content type: JSON');
          			break;
          			case 'application/x-www-form-urlencoded':
          				to_log('Content type: x-www-form-urlencoded');
          				$payload = urldecode($payload);
          				$payload = substr($payload, 8); // remove "payload= "
          			break;
          			default:
          				stop('Unsupported content type: ' . htmlspecialchars($_SERVER['CONTENT_TYPE']));
          		}
          		
          		$payload = json_decode($payload);		
          	}
          	else {
          		stop('Content type mismatch');
          	}
          
          
          	// Auto pull live if push was on master branch
          	if ($payload->{'ref'} === BRANCH) {
          		to_log("Branch: " . BRANCH); // Log and continue
          	}
          	else {
          		stop("Warning: Pushed branch does not match " . BRANCH); // Exit
          	}
          
          
          	// Check for repository
          	$rep_name = ROOT .  htmlspecialchars($payload->{'repository'}->{'name'});
          
          	if (is_dir($rep_name) && file_exists("$rep_name/.git")) {
          		to_log("Repository: " . htmlspecialchars($payload->{'repository'}->{'name'})); // Log and continue		
          		chdir($rep_name); // сменить директорию
          	}
          	else {
          		stop("$rep_name is not a repository"); // Exit
          	}
          
          
          	// Pull
          	// (2>&1 - перенаправить поток вывода ошибок STDERR &2 в стандартный вывод STDOUT &1
          	// чтобы в переменную $pull_result попадал также результат неудачного выполнения команды)
          	try {
          		to_log("AUTO PULL INITIATED");
          		
          		$pull_result = '# git fetch --all: ' . shell_exec('git fetch --all 2>&1');
          		$pull_result .= '# git checkout --force "origin/master": ' . shell_exec('git checkout --force "origin/master" 2>&1');
          
          		to_log($pull_result."AUTO PULL COMPLETE");
          	}
          	catch (Exception $e) {
          		stop("Error during auto pull - $e");
          	}
          
          	ok();
          
          }
          
          
          // To Log progress
          function to_log($data = '') {
          	$log = fopen(LOGFILE, "a");
          	if (!$log){
          		exit("Can not open log file to append data");
          	}
          	
          	fwrite($log, $data."\n");
          	fclose($log);
          }
          
          // To forbid access
          function stop($reason = '') {
          	$log = fopen(LOGFILE, "a");
          	if (!$log){
          		exit("Can not open log file to append data");
          	}
          	
              fwrite($log, "=== ERROR: $reason  ===\n\n");
              fclose($log);
          	
              header("HTTP/1.0 403 Forbidden");
          
              exit();
          }
          
          // function to return OK
          function ok() {
          	to_log();
          	
              ob_start();
              header("HTTP/1.1 200 OK");
              header("Connection: close");
              header("Content-Length: " . ob_get_length());
              ob_end_flush();
              ob_flush();
              flush();
          }
          
          ?>

            0

            Ваш скрипт решает вопрос с внешними за зависимостями?

            0

            Не понятно, каким образом это избавит от появления гайзенбагов?

              0

              Поясните более подробно, пожалуйста.


              Дополнительно автору:


              Этот файл всегда называется именно так и не имеет расширения

              ну-ну. Можно переопределить имя Dockerfile, а так же положить его в другом каталоге. Но при этом при docker build придется передавать путь и имя файла (через -f)

                0
                Ни от аппаратной ошибки это не спасёт, ни от ошибки проектирования (RC, на пример).

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое