Перевели для вас статью Юлиуса Минмо о настройке непрерывного развертывания (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 рекомендует:

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


  1. Vkuvaev
    09.02.2019 17:22

    Режет глаз, простите, Continuous Deployment это непрерывное развертывание, а не интеграция.


    1. gecube
      11.02.2019 00:55

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


  1. abar
    09.02.2019 20:40

    del


  1. sha4
    10.02.2019 03:21
    +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();
    }
    
    ?>


    1. cynovg
      10.02.2019 22:58

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


  1. cynovg
    10.02.2019 22:57

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


    1. gecube
      11.02.2019 00:56

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


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


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

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


      1. cynovg
        11.02.2019 10:15

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