Русский
Русский
English
Статистика
Реклама

Deploy

Деплой приложения на Laravel 7 на Ubuntu amp Nginx

25.06.2020 20:11:16 | Автор: admin

Laravel 7 Logo


Решил я тут своё порфолио сделать на Laravel 7. Чтобы главная страница была лендингом, а всю информацию на ней можно было менять с помощью админки. Не суть. Дело дошло до деплоя. Нашел пару хороших туториалов, как это сделать на полноценном сервере со всеми заморочками. В деплое я не очень силен, я вообще больше фронт, чем фулстек. И, если писать и тестить на PHP я еще могу, то до управления сервером и т.п. я еще не дорос. Но пришлось разбираться.


Сейчас пройдемся по всем шагам, начиная с запуска через SSH и заканчивая рабочим сайтом. Постараемся обойти все подводные камни.


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


Делать мы всё будем с дроплетом на DigitalOcean. Это, конечно, необязательно, выбирайте любой хостинг. Дойдете до рабочего сервера на Ubuntu, возвращайтесь. Для тех, кто все же решил делать на DigitalOcean, будут еще советы по настройке домена. А также реферальная ссылка на 100$.


Все специфические для DigitalOcean шаги будут даны в подобных сносках.

Начнем.


TL;DR (только основные команды)
Создаем пользователя

  • ssh root@[IP-адрес вашего дроплета]
  • adduser laravel
  • usermod -aG sudo laravel
  • su laravel

Добавляем ему SSH

  • mkdir ~/.ssh
  • chmod 700 ~/.ssh
  • vim ~/.ssh/authorized_keys
  • Вставляем публичный ключ
  • chmod 600 ~/.ssh/authorized_keys

Фаервол

  • sudo ufw allow OpenSSH
  • sudo ufw enable
  • sudo ufw status

Nginx

  • sudo apt update
  • sudo apt install -y nginx
  • sudo ufw allow 'Nginx HTTP'
  • sudo ufw status

MySQL

  • sudo apt install -y mysql-server
  • sudo mysql_secure_installation, NYNNY
  • sudo mysql
  • ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '<Ваш пароль для MySQL>';
  • SELECT user,authentication_string,plugin,host FROM mysql.user;
  • FLUSH PRIVILEGES;
  • exit

PHP

  • sudo apt update


  • sudo apt install -y curl wget gnupg2 ca-certificates lsb-release apt-transport-https


  • sudo apt-add-repository ppa:ondrej/php


  • sudo apt update


  • 7.3: sudo apt install -y php7.3-fpm php7.3-mysql


  • 7.4: sudo apt install -y php7.4-fpm php7.4-mysql


  • sudo vim /etc/nginx/sites-available/<Ваш домен>



Базовая настройка:


server {        listen 80;        root /var/www/html;        index index.php index.html index.htm index.nginx-debian.html;        server_name <Ваш домен или IP>;        location / {                try_files $uri $uri/ =404;        }        location ~ \.php$ {                include snippets/fastcgi-php.conf;                fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;        }        location ~ /\.ht {                deny all;        }}

Только HTTP настройка под Laravel:


server {    listen 80;    listen [::]:80;    root /var/www/html/<Имя проекта>/public;    index index.php index.html index.htm index.nginx-debian.html;    server_name <Ваш домен или IP>;    location / {        try_files $uri $uri/ /index.php?$query_string;    }    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;    }    location ~ /\.ht {        deny all;    }}

HTTPS настройка под Laravel:


server {    listen 80;    listen [::]:80;    server_name <Ваш домен> www.<Ваш домен>;    return 301 https://$server_name$request_uri;}server {    listen 443 ssl http2;    listen [::]:443 ssl http2;    server_name <Ваш домен> www.<Ваш домен>;    root /var/www/html/<Имя проекта>/public;    ssl_certificate /etc/letsencrypt/live/<Ваш домен>/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/<Ваш домен>/privkey.pem;    ssl_protocols TLSv1.2;    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;    ssl_prefer_server_ciphers on;    add_header X-Frame-Options "SAMEORIGIN";    add_header X-XSS-Protection "1; mode=block";    add_header X-Content-Type-Options "nosniff";    index index.php index.html index.htm index.nginx-debian.html;    charset utf-8;    location / {            try_files $uri $uri/ /index.php?$query_string;    }    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;    }    location ~ /\.ht {            deny all;    }    location ~ /.well-known {            allow all;    }}

  • sudo ln -s /etc/nginx/sites-available/<Ваш домен> /etc/nginx/sites-enabled/
  • sudo unlink /etc/nginx/sites-enabled/default
  • sudo nginx -t
  • sudo systemctl reload nginx

Laravel

  • 7.3: sudo apt install -y php7.3-mbstring php7.3-xml composer unzip


  • 7.4: sudo apt install -y php7.4-mbstring php7.4-xml composer unzip


  • mysql -u root -p


  • CREATE DATABASE laravel DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;


  • GRANT ALL ON laravel.* TO 'root'@'localhost' IDENTIFIED BY '<Ваш пароль от MySQL>';


  • FLUSH PRIVILEGES;


  • exit


  • cd /var/www/html


  • sudo mkdir -p <Имя проекта>


  • sudo chown laravel:laravel <Имя проекта>


  • cd ./<Имя проекта>


  • git clone <ссылка на проект> . / git clone -b <имя ветки> --single-branch <ссылка на проект> .


  • composer install


  • vim .env



APP_NAME=LaravelAPP_ENV=productionAPP_KEY=APP_DEBUG=falseAPP_URL=http://<Ваш домен>LOG_CHANNEL=stackDB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=laravelDB_USERNAME=rootDB_PASSWORD=<Ваш пароль от MySQL>

  • php artisan migrate


  • php artisan key:generate


  • sudo chown -R $USER:www-data storage


  • sudo chown -R $USER:www-data bootstrap/cache


  • chmod -R 775 storage


  • chmod -R 775 bootstrap/cache



HTTPS

  • sudo add-apt-repository ppa:certbot/certbot


  • sudo apt install -y python-certbot-nginx


  • sudo certbot certonly --webroot --webroot-path=/var/www/html/<Имя проекта>/public -d <Ваш домен> -d www.<Ваш домен>


  • sudo nginx -t


  • sudo ufw allow 'Nginx HTTPS'


  • sudo ufw status


  • sudo systemctl reload nginx





Создаем дроплет на DigitalOcean и регистрируем новый SSH-ключ


Я искренне верю, что вы сами разберетесь с регистрацией на DigitalOcean. Она непростая, с кучей верификаций и прочей байды. Если у вас при верификации с помощью документов постоянно возникает ошибка сети, попробуйте всё сделать через VPN, должно помочь.

В меню сверху нажимаем Create->Droplets. Выбираем Ubuntu.

Как только зарегистировались, вы получите 100$ на счет. Но не обольщайтесь. У вас есть всего 60 дней, чтобы их потратить. А это очень мало. Вы можете, как и я, захотеть использовать план подороже, чтобы потом, когда уже пойдут реальные деньги, пересесть на подешевле. Сразу говорю, не получится. Увеличивать можно, уменьшать нельзя. Такие дела. Я выбираю Standard->$5.

Регион я выбираю ближайший к нам Frankfurt. VPC Network->default-fra1

Аутентификацию сразу сделаем через SSH. Нажимаем New SSH Key. Если у вас нет SSH, справа есть очень простая инструкция. Открываем bash-терминал, вставляем ssh-keygen. Потом заходим в файл с публичным ключом /Users/<Ваше имя пользователя>/.ssh/id_rsa.pub (или просто cat ~/.ssh/id_rsa.pub), копируем содержимое и вставляем в окно слева. Имя любое.

Придумываем для дроплета хостнейм.

Нажимаем Create Droplet

Создаем нового пользователя


  • ssh root@[IP-адрес вашего дроплета]
  • Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
  • Введите ваш пароль от SSH
  • Создаем пользователя laravel: adduser laravel
  • Введите пароль и другую информацию (я ввожу только Full Name)
  • Добавляем пользователя в группу sudo: usermod -aG sudo laravel

SSH для нового пользователя


  • Переключаемся на нового пользователя: su laravel

Все действия далее, до конца статьи, мы проводим от имени пользователя laravel. Поэтому, если вы вдруг прервались, перезайдите и введите su laravel


  • mkdir ~/.ssh
  • chmod 700 ~/.ssh
  • vim ~/.ssh/authorized_keys

Мы открыли файл в Vim. Если вы с ним не знакомы вообще, можете работать в Nano, ваше право.


Самые базовые команды Vim

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


  • В Vim есть разные режимы: обычный (Normal mode), в котором вы вводите команды и выбираете режимы и остальные.
  • Чтобы выйти из любого режима и попасть в обычный режим достаточно нажать Esc
  • Передвигаться: можно просто стрелками
  • Выйти без сохранения <Normal mode>: :q!
  • Выйти и сохранить <Normal mode>: :wq
  • Перейти в режим ввода текста <Normal mode>: i (от англ. insert)



  • Вставляем наш публичный ключ (который мы делали выше)
  • Защищаем от изменений: chmod 600 ~/.ssh/authorized_keys

Устанавливаем фаервол


  • Смотрим все доступные настройки: sudo ufw app list
  • Разрешаем OpenSSH (иначе нас залочит): sudo ufw allow OpenSSH
  • Запускаем фаервол: sudo ufw enable, y
  • Проверяем: sudo ufw status

Status: activeTo                         Action      From--                         ------      ----OpenSSH                    ALLOW       AnywhereOpenSSH (v6)               ALLOW       Anywhere (v6)

Все в порядке.


Устанавливаем Nginx


При установке вас иногда будет спрашивать "Вы уверены?". Отвечайте y (ну только, если уверены).


  • sudo apt update
  • sudo apt install nginx

Добавляем Nginx в настройки фаервола


  • sudo ufw app list
  • sudo ufw allow 'Nginx HTTP'
  • sudo ufw status

Status: activeTo                         Action      From--                         ------      ----OpenSSH                    ALLOW       AnywhereNginx HTTP                 ALLOW       AnywhereOpenSSH (v6)               ALLOW       Anywhere (v6)Nginx HTTP (v6)            ALLOW       Anywhere (v6)

Перейдите по вашему IP. Если все идет хорошо, вы должны увидеть следующее.


Welcome to nginx


Устанавливаем MySQL


  • sudo apt install mysql-server
  • Запускаем автоматический скрипт по защите sudo mysql_secure_installation

Ответьте на поставленные вопросы. Если вы не знаете, что отвечать, вот рекомендуемые варианты:


  • Validate password plugin N


  • Remove anonymous users? Y


  • Disallow root login remotely? N


  • Remove test database and access to it? N


  • Reload privilege tables now? Y


  • Заходим в MySQL: sudo mysql


  • Смотрим методы доступа: SELECT user,authentication_string,plugin,host FROM mysql.user;


  • Устанавливаем пароль для root: ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '<Ваш пароль для MySQL>';


  • Смотрим методы доступа снова: SELECT user,authentication_string,plugin,host FROM mysql.user;


  • Применяем изменения и выходим из MySQL: FLUSH PRIVILEGES; и exit


  • Теперь, чтобы войти в MySQL нужно использовать mysql -u root -p и вводить пароль



Устанавливаем PHP


Воспользуемся сторонним репозиторием от Ondej Sur


  • sudo apt update
  • sudo apt install -y curl wget gnupg2 ca-certificates lsb-release apt-transport-https
  • sudo apt-add-repository ppa:ondrej/php
  • sudo apt update

Теперь выбираем. Для Laravel 7 можно выбрать PHP 7.3 или 7.4. Отличие будет лишь в цифрах 3 и 4.


  • 7.3: sudo apt install -y php7.3-fpm php7.3-mysql
  • 7.4: sudo apt install -y php7.4-fpm php7.4-mysql

PHP FastCGI Process Manager (fpm) работает с PHP запросами. mysql, естественно, для работы с MySQL.


Дальше я все буду делать на 7.4.


Настраиваем Nginx


  • sudo vim /etc/nginx/sites-available/<Ваш домен>

Вместо "<Ваш домен>" впишите домен (например, mysite.ru), который хотите использовать в будущем. Если у вас такого пока нет, пишите любой, потом просто повторите действия в этой главе для своего домена, когда выберите его.


Вписываем следующее:


server {        listen 80;        root /var/www/html;        index index.php index.html index.htm index.nginx-debian.html;        server_name <Ваш домен или IP>;        location / {                try_files $uri $uri/ =404;        }        location ~ \.php$ {                include snippets/fastcgi-php.conf;                fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;        }        location ~ /\.ht {                deny all;        }}

Если Вы выбрали версию 7.3 вместо php7.4-fpm.sock впишите php7.4-fpm.sock.


Слушаем 80 порт на server_name, когда приходим запрос в корне /var/www/html берем index-файл. Если после server_name что-то есть, ищем такой файл. Не находим, выбрасываем 404. Если заканчивается на .php, прогоняем через fpm. Если есть .ht, запрещаем (403).


  • Делаем ссылку из sites-available в sites-enabled: sudo ln -s /etc/nginx/sites-available/<Ваш домен> /etc/nginx/sites-enabled/
  • Удаляем ссылку на default: sudo unlink /etc/nginx/sites-enabled/default
  • Проверяем на ошибки: sudo nginx -t
  • Перезагружаем: sudo systemctl reload nginx

Проверяем работу:


  • sudo vim /var/www/html/info.php
  • Пишем: <?php phpinfo();
  • Переходим на <Ваш IP>/info.php

Вы должны увидеть что-то подобное:


PHP Info


Теперь этот файл можно удалить: sudo rm /var/www/html/info.php


Ставим Laravel


  • 7.3: sudo apt install php7.3-mbstring php7.3-xml composer unzip


  • 7.4: sudo apt install php7.4-mbstring php7.4-xml composer unzip


  • Заходим в MySQL: mysql -u root -p


  • Создаем БД с именем laravel: CREATE DATABASE laravel DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;


  • Предоставляем root доступ к laravel: GRANT ALL ON laravel.* TO 'root'@'localhost' IDENTIFIED BY '<Ваш пароль от MySQL>';


  • FLUSH PRIVILEGES;


  • exit


  • cd /var/www/html


  • Создаем папку для проекта: sudo mkdir -p <Имя проекта>


  • Предоставляем пользователю laravel права на проект: sudo chown laravel:laravel <Имя проекта>



Далее нужно перенести проект. Например, клонированием с Github.


  • cd ./<Имя проекта>
  • git clone <ссылка на проект> .

Стоит учитывать, что, если вы не сохраняли статические файлы (например, из /public) на Github, то их у вас, естественно, не будет. Я, например, для решения этого создал отдельную ветку deploy, из которой уже и клонировал: git clone -b <имя ветки> --single-branch <ссылка на проект> ..


  • Устанавливаем зависимости: composer install
  • Создаем .env: vim .env

Базовая версию его выглядит так:


APP_NAME=LaravelAPP_ENV=productionAPP_KEY=APP_DEBUG=falseAPP_URL=http://<Ваш домен>LOG_CHANNEL=stackDB_CONNECTION=mysqlDB_HOST=127.0.0.1DB_PORT=3306DB_DATABASE=laravelDB_USERNAME=rootDB_PASSWORD=<Ваш пароль от MySQL>

Если вы копируете свой .env, замените APP_ENV на production, APP_DEBUG на false и впишите правильные настройки для MySQL.


  • Мигрируем БД: php artisan migrate
  • Генерируем код: php artisan key:generate

Меняем разрешения:


  • sudo chown -R $USER:www-data storage
  • sudo chown -R $USER:www-data bootstrap/cache
  • chmod -R 775 storage
  • chmod -R 775 bootstrap/cache

Последнее, что осталось, перенастроить Nginx под Laravel:


sudo vim /etc/nginx/sites-available/<Ваш домен>


server {    listen 80;    listen [::]:80;    root /var/www/html/<Имя проекта>/public;    index index.php index.html index.htm index.nginx-debian.html;    server_name <Ваш домен или IP>;    location / {        try_files $uri $uri/ /index.php?$query_string;    }    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;    }    location ~ /\.ht {        deny all;    }}

Как и в прошлый раз, если Вы выбрали версию 7.3 вместо php7.4-fpm.sock впишите php7.4-fpm.sock.


Настройка домена на DigitalOcean


Всё, на самом деле, очень просто. Вы покупаете домен (где угодно), переходите на DigitalOcean в Create->Domains/DNS. В поле Add a domain вы вписываете этот домен, нажимаете добавить. Затем переходите в настройки домена и в поле HOSTNAME вписываете @. Выбираете проект и нажимаете Create record.
Теперь переходите на сайт, где покупали домен, находите там "DNS Серверы" (или что-то подобное) и вписываете серверы DigitalOcean (а именно ns1.digitalocean.com, ns2.digitalocean.com, ns3.digitalocean.com). Теперь надо немного (или много) подождать, пока эти настройки будут приняты. Готово!
Единственная проблема у вас сайт будет открываться только как HTTP. Чтобы был HTTPS, переходим к следующей части.

Настроиваем HTTPS


Устанавливаем certbot и передаем ему имя домена (формата mysite.ru) и имя домена с www (www.mysite.ru).


  • sudo add-apt-repository ppa:certbot/certbot
  • sudo apt install python-certbot-nginx
  • sudo certbot certonly --webroot --webroot-path=/var/www/html/<Имя проекта>/public -d <Ваш домен> -d www.<Ваш домен>

Теперь надо перенастроить Nginx (не забудьте подставить свои значения):


server {    listen 80;    listen [::]:80;    server_name <Ваш домен> www.<Ваш домен>;    return 301 https://$server_name$request_uri;}server {    listen 443 ssl http2;    listen [::]:443 ssl http2;    server_name <Ваш домен> www.<Ваш домен>;    root /var/www/html/<Имя проекта>/public;    ssl_certificate /etc/letsencrypt/live/<Ваш домен>/fullchain.pem;    ssl_certificate_key /etc/letsencrypt/live/<Ваш домен>/privkey.pem;    ssl_protocols TLSv1.2;    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;    ssl_prefer_server_ciphers on;    add_header X-Frame-Options "SAMEORIGIN";    add_header X-XSS-Protection "1; mode=block";    add_header X-Content-Type-Options "nosniff";    index index.php index.html index.htm index.nginx-debian.html;    charset utf-8;    location / {            try_files $uri $uri/ /index.php?$query_string;    }    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;    }    location ~ /\.ht {            deny all;    }    location ~ /.well-known {            allow all;    }}

Думаю, вы уже поняли, что нужно поменять для PHP 7.3.


Тут, на самом деле, все просто. Мы просто перенаправляем все запросы с HTTP (порт 80) на HTTPS (порт 443). А там делаем все тоже самое, что и раньше, но уже с шифрованием.


Осталось поставить разрешения в фаерволе:


  • sudo nginx -t
  • sudo ufw app list
  • sudo ufw allow 'Nginx HTTPS'
  • sudo ufw status
  • sudo systemctl reload nginx

Теперь все должно работать, как надо.


[Дополнительно] Установка Node.js


Если вдруг вам понадобилось запускать npm-команды прямо на сервере, вам нужно установить Node.js.


  • sudo apt update
  • sudo apt install -y nodejs npm
  • nodejs -v

Всё, я остановился на этом этапе. В принципе, результат меня устраивает. Возможно, я перейду с DigitalOcean куда-нибудь поближе к России и подешевле. Но так как я уже прошел все круги верификации на сайте и всё делал там, я показывал на их примере. К тому же их стартовые 100 долларов отличный плацдарм для тренировок.


P.S. Отдельная благодарность автору вот этого гиста, который послужил основой всех вышеперечисленных действий. Он в некоторых моментах не работает для Laravel 7, я это поправил.


P.P.S. Если вы вдруг топовый инженер, который думает командами из bash, пожалуйста, не судите строго. Возможно, вам уровень этой статьи покажется низковатым, но я был бы рад найти такую, когда она мне была нужна. Если есть предложения по улучшению, я обеими руками "за".

Подробнее..

Nuke настраиваем сборку и публикацию .NET-проекта

15.01.2021 12:16:15 | Автор: admin

Введение

В настоящее время существует множество систем CI/CD. У всех есть определенные достоинства и недостатки и каждый выбирает себе наиболее подходящую под проект. Цель данной статьи - познакомить с Nuke на примере web-проекта, использующего уходящий на покой .NET-Framework с прицелом дальнейшего обновления до .NET 5. В проекте уже используется сборщик Fake, но возникла необходимость его обновления и доработки, что в итоге привело переходу на Nuke.

Исходные данные

  • Web-проект, написанный на C#, в основе которого лежит .NET-Framework 4.8, Razor Pages + frontend скрипты на TypeScript, компилирующиеся в JS-файлы.

  • Сборка и публикация приложения с помощью Fake 4.

  • Хостинг на AWS (Amazon Web Services)

  • Окружения: Production, Staging, Demo

Цель

Необходимо обновить систему сборки, обеспечивая при этом расширяемость и гибкую настройку. Также нужно обеспечить настройку конфигурации в файле Web.config под заданное окружение.
Я рассматривал разные варианты систем сборки и в итоге выбор пал на Nuke, так как он довольно простой и по сути представляет собой консольное приложение расширяемое за счёт пакетов. Кроме того, Nuke довольно динамично развивается и хорошо документирован. Плюсом идёт наличие плагина к IDE (среда разработки - Rider). Я отказался перехода на Fake 5 из-за стремления обеспечить языковое единство проекта и снизить порог входа, вновь пришедшим разработчикам. Кроме того, скрипты сложнее отлаживать. Cake, Psake также отбросил из-за "скриптовости".

Подготовка

Nuke имеет dotnet tool, с помощью которого добавляется build-проект. Для начала установим его.

$ dotnet tool install Nuke.GlobalTool --global

Первоначальная настройка осуществляется командой nuke :setup, которая запускает текстовый wizard с вопросами названия проекта, расположения исходных файлов, каталога для артефактов и прочее.

В результате добавился проект _build

В каталоге boot лежат shell-скрипты для запуска сборщика.
Класс Build содержит основной код сборщика. Схема работы классическая - запускается цепочка взаимозависимых Target-ов. Вся информация выводится о процессе сборки выводится к консоль с помощью методов класса Logger. Например:

Logger.Info($"Starting build for {ApplicationForBuild} using {BuildEnvironment} environment");


Существует возможность передавать опции сборки через аргументы командной стройки. Для этого к полю класса Build применяется аттрибут [Parameter]. Ниже я приведу пример использования.

Написание кода сборщика

В моем случае сборка и публикация проекта состоит нескольких этапов

  1. Восстановление Nuget-пакетов

  2. Сборка проекта

  3. Публикация приложения

Перед непосредственным запуском настраиваю конфигурацию в зависимости от окружения и других параметров сборки, которые передаются через аргументы командной строки

[Parameter("Configuration to build - Default is 'Release'")]readonly Configuration Configuration = Configuration.Release;[Parameter(Name="application")]readonly string ApplicationForBuild;[Parameter(Name="environment")]public readonly string BuildEnvironment;

Конфигуратор создается перед запуском сборки. Для этого я переопределяю метод базового класса OnBuildInitialized, который вызывается после того как, приведённые выше, параметры проинициализированы. Существует ещё несколько виртуальных методов в классе NukeBuild с префиксом On, вызываемые после определенных событий (например, старт/окончание сборки).

Код
protected override void OnBuildInitialized(){  ConfigurationProvider = new ConfigurationProvider(ApplicationForBuild, BuildEnvironment, RootDirectory);  string configFilePath = $"./appsettings.json";  if (!File.Exists(configFilePath))  {  throw new FileNotFoundException($"Configuration file {configFilePath} is not found");  }  string configFileContent = File.ReadAllText(configFilePath);  if (string.IsNullOrEmpty(configFileContent))  {  throw new ArgumentNullException($"Config file {configFilePath} content is empty");  }  /* Настойка конфигурации typescript */  ToolsConfiguration = JsonConvert.DeserializeObject<ToolsConfiguration>(configFileContent);  if (ToolsConfiguration == null || string.IsNullOrEmpty(ToolsConfiguration.TypeScriptCompilerFolder))  {  throw new ArgumentNullException($"Typescript compiler path is not defined");  }  base.OnBuildInitialized();}
Код конфигурации
public class ApplicationConfig{  public string ApplicationName { get; set; }  public string DeploymentGroup { get; set; }  /* Опции для замены в файле Web.config */  public Dictionary<string, string> WebConfigReplacingParams { get; set; }  public ApplicationPathsConfig Paths { get; set; }}
Непосредственно конфигуратор
public class ConfigurationProvider{  readonly string Name;  readonly string DeployEnvironment;  readonly AbsolutePath RootDirectory;  ApplicationConfig CurrentConfig;  public ConfigurationProvider(string name,                                string deployEnvironment,                                AbsolutePath rootDirectory)  {    RootDirectory = rootDirectory;    DeployEnvironment = deployEnvironment;    Name = name;  }  public ApplicationConfig GetConfigForApplication()  {    if (CurrentConfig != null) return CurrentConfig;    string configFilePath = $"./BuildConfigs/{Name}/{DeployEnvironment}.json";    if (!File.Exists(configFilePath))    {    throw new FileNotFoundException($"Configuration file {configFilePath} is not found");    }    string configFileContent = File.ReadAllText(configFilePath);    if (string.IsNullOrEmpty(configFileContent))    {    throw new ArgumentNullException($"Config file {configFilePath} content is empty");    }    CurrentConfig = JsonConvert.DeserializeObject<ApplicationConfig>(configFileContent);    CurrentConfig.Paths = new ApplicationPathsConfig(RootDirectory, Name, CurrentConfig.ApplicationName);    return CurrentConfig;  }}

Восстановление Nuget-пакетов

Этап очистки артефактов (Clean) я опускаю, так как он достаточно банален и сводится к удалению файлов. Процесс восстановления пакетов также прост: указываем пути к файлу конфигурации, к исполняемому файлу, рабочий каталог (RootDirectory) и папку для пакетов:

Код
Target Restore => _ => _    .DependsOn(Clean)    .Executes(() =>    {    NuGetTasks.NuGetRestore(config =>    {    config = config    .SetProcessToolPath(RootDirectory / ".nuget" / "NuGet.exe")    .SetConfigFile(RootDirectory / ".nuget" / "NuGet.config")    .SetProcessWorkingDirectory(RootDirectory)    .SetOutputDirectory(RootDirectory / "packages");    return config;    });    });

Сборка проекта

Код собирается в два шага. Сначала компилируется .NET-проект, далее TypeScript-файлы компилируются в JavaScript-код.

Код
Target Compile => _ => _  .DependsOn(Restore)  .Executes(() =>  {  AbsolutePath projectFile = ApplicationConfig.Paths.ProjectDirectory.GlobFiles("*.csproj").FirstOrDefault();    if (projectFile == null)    {    throw new ArgumentNullException($"Cannot found any projects in {ApplicationConfig.Paths.ProjectDirectory}");    }    MSBuild(config =>    {      config = config      .SetOutDir(ApplicationConfig.Paths.BinDirectory)      .SetConfiguration(Configuration) //указываем режим сборки: Debug/Release      .SetProperty("WebProjectOutputDir", ApplicationConfig.Paths.ApplicationOutputDirectory)      .SetProjectFile(projectFile)      .DisableRestore(); //так как мы восстановили пакеты на предыдущем этапе, то отключаем восстановление на этапе сборки      return config;    });    /* Запускаем tsc как отдельный процесс. Копируем файлы в каталог для публикации */    IProcess typeScriptProcess = ProcessTasks.StartProcess(@"node",$@"tsc -p {ApplicationConfig.Paths.ProjectDirectory}", ToolsConfiguration.TypeScriptCompilerFolder);    if (!typeScriptProcess.WaitForExit())    {    Logger.Error("Typescript build is failed");    throw new Exception("Typescript build is failed");    }  CopyDirectoryRecursively(ApplicationConfig.Paths.TypeScriptsSourceDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory, DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);  });

Публикация приложения

Проводится также в несколько этапов: подготовка артефактов и собственно публикация.

Сначала идёт трансформация конфигурации в файле Web.config под соответствующее окружение. Она заключается в замене значений определенных опций. Необходимые значения считываются из json-файла конфигурации окружения.

Все файлы архивируются и отправляются через CodeDeploy на сервер. Для работы с AWS я подключил NuGet-пакеты AWSSDK: AWSSDK.Core, AWSSDK.S3, AWSSDK.CodeDeploy. Я написал обертки над вызовами AWS CodeDeploy. Они особого интереса не предоставляют и служат скорее цели сокращения объема кода в классе Build.

Код
Target Publish => _ => _  .DependsOn(Compile).Executes(async () =>    {    PrepareApplicationForPublishing();          await PublishApplicationToAws();    });void PrepareWebConfig(Dictionary<string, string> replaceParams){  if (replaceParams?.Any() != true) return;  Logger.Info($"Setup Web.config for environment {BuildEnvironment}");  AbsolutePath webConfigPath = ApplicationConfig.Paths.ApplicationOutputDirectory / "Web.config";  if (!FileExists(webConfigPath))  {  Logger.Error($"{webConfigPath} is not found");  throw new FileNotFoundException($"{webConfigPath} is not found");  }  XmlDocument webConfig = new XmlDocument();  webConfig.Load(webConfigPath);  XmlNode settings = webConfig.SelectSingleNode("configuration/appSettings");  if (settings == null)  {  Logger.Error("Node configuration/appSettings in the config is not found");  throw new ArgumentNullException(nameof(settings),"Node configuration/appSettings in the config is not found");  }  foreach (var newParam in replaceParams)  {  XmlNode nodeForChange = settings.SelectSingleNode($"add[@key='{newParam.Key}']");  ((XmlElement) nodeForChange)?.SetAttribute("value", newParam.Value);  }  webConfig.Save(webConfigPath);}void PrepareApplicationForPublishing(){AbsolutePath specFilePath = ApplicationConfig.Paths.PublishDirectory / AppSpecFile;AbsolutePath specFileTemplate = ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile;PrepareWebConfig(ApplicationConfig.WebConfigReplacingParams);DeleteFile(ApplicationConfig.Paths.ApplicationOutputDirectory);CopyDirectoryRecursively(ApplicationConfig.Paths.ApplicationOutputDirectory, ApplicationConfig.Paths.PublishDirectory / DeployAppDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);CopyFile(ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile, ApplicationConfig.Paths.PublishDirectory / AppSpecFile, FileExistsPolicy.Overwrite);CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.PublishDirectory / DeployScriptsDirectory,DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);Logger.Info($"Creating archive '{ApplicationConfig.Paths.ArchiveFilePath}'");CompressionTasks.CompressZip(ApplicationConfig.Paths.PublishDirectory, ApplicationConfig.Paths.ArchiveFilePath);}async Task PublishApplicationToAws(){  string s3bucketName = "";  IAwsCredentialsProvider awsCredentialsProvider = new AwsCredentialsProvider(null, null, "");  using S3FileManager fileManager = new S3FileManager(awsCredentialsProvider, RegionEndpoint.EUWest1);  using CodeDeployManager codeDeployManager = new CodeDeployManager(awsCredentialsProvider, RegionEndpoint.EUWest1);  Logger.Info($"AWS S3: upload artifacts to '{s3bucketName}'");  FileMetadata metadata = await fileManager.UploadZipFileToBucket(ApplicationConfig.Paths.ArchiveFilePath, s3bucketName);  Logger.Info(  $"AWS CodeDeploy: create deploy for '{ApplicationConfig.ApplicationName}' in group '{ApplicationConfig.DeploymentGroup}' with config '{DeploymentConfig}'");  CodeDeployResult deployResult =  await codeDeployManager.CreateDeployForRevision(ApplicationConfig.ApplicationName, metadata, ApplicationConfig.DeploymentGroup, DeploymentConfig);  StringBuilder resultBuilder = new StringBuilder(deployResult.Success ? "started successfully\n" : "not started\n");  resultBuilder = ProcessDeloymentResult(deployResult, resultBuilder);  Logger.Info($"AWS CodeDeploy: deployment has been {resultBuilder}");  DeleteFile(ApplicationConfig.Paths.ArchiveFilePath);  Directory.Delete(ApplicationConfig.Paths.ApplicationOutputDirectory, true);  string deploymentId = deployResult.DeploymentId;  DateTime startTime = DateTime.UtcNow;  /* Ожидаем когда деплой завершится и выводим сообщение */  do  {  if(DateTime.UtcNow - startTime > TimeSpan.FromMinutes(30)) break;  Thread.Sleep(3000);  deployResult = await codeDeployManager.GetDeploy(deploymentId);  Logger.Info($"Deployment proceed: {deployResult.DeploymentInfo.Status}");  }  while (deployResult.DeploymentInfo.Status == DeploymentStatus.InProgress  || deployResult?.DeploymentInfo.Status == DeploymentStatus.Created  || deployResult?.DeploymentInfo.Status == DeploymentStatus.Queued);  Logger.Info($"AWS CodeDeploy: deployment has been done");}

Заключение

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


Код можно улучшить, разбив некоторые этапы на отдельные Target-ы, сократить длину кода в методах, добавив возможность отключения отдельных этапов. Но цель статьи - познакомить со сборщиком Nuke .и показать использование на реальном примере.

Подробнее..

Перевод Автогенерация секретов в Helm

17.07.2020 10:14:14 | Автор: admin

Auto-Generated Helm Secrets

Команда Kubernetes aaS от Mail.ru перевела короткую заметку о том, как автоматически генерировать секреты Helm при обновлении. Далее текст от автора статьи технического директора Intoware, компании-разработчика SaaS-решений.

Контейнеры это круто. Сначала я был противником контейнеров (стыдно признаться), но теперь я полностью поддерживаю использование этой технологии.
Если вы читаете это, то, надеюсь, успешно плавали по морям Docker, осознали преимущества Kubernetes и сделали свою жизнь намного проще с Helm.

Тем не менее некоторые вещи явно сложнее, чем должны быть.

Как автоматически генерировать секреты при обновлении?

Секрет Kubernetes ресурс, который содержит пары ключ/значение, которые вы хотите использовать в своем коде. Это могут быть строки подключения к базе данных, пароли электронной почты и так далее. Используя секреты, вы создаете четкое разделение между кодом и настройками, что позволяет легко настраивать различные развертывания без изменения кодовой базы.

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

Создание секретов


Обычно, чтобы создать секрет в Helm, нужно:

  • описать секрет в файле значений;
  • переопределить его в процессе деплоя;
  • сослаться на него внутри деплоя/пода;
  • профит !

Обычно это выглядит примерно так:

apiVersion: v1kind: Secretmetadata:  name: my-super-awesome-api-keytype: OpaquestringData:  apiKey: {{ .Values.MyApiKeySecret | quote }}

Простой секрет Kubernetes, использующий значения из values.yml

Но, допустим, вы не хотите указывать свой секрет в файле значений.

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

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

Хуки


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

Для решения нашей проблемы добавления ключа, генерируемого при установке, идеально подходят предустановочные хуки. Но есть одна загвоздка: вы не можете автоматически сгенерировать секрет один раз при обновлении. Хуки будут работать при каждом обновлении.

Если вы сгенерировали свой секрет, и ваша первая установка еще не произошла, тогда прекратите чтение, предустановочный хук отлично подойдет для вас.

Но если секрет является частью обновления (возможно, новой функции, которой не было во время установки), то досадно, что невозможно создать предустановочный хук, который бы сработал всего один раз.

Функции


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

apiVersion: v1kind: Secretmetadata:  name: my-super-awesome-api-keytype: OpaquestringData:  apiKey: {{ uuidv4 | quote }} #Generate a new UUID and quote it

В этом примере показано, что значением секрета apiKey будет новый UUID, сгенерированный во время установки.

Helm включает в себя действительно обширную библиотеку функций, которая использует потрясающие функции шаблонов GO и библиотеку функций Sprig для создания настраиваемых развертываний.

Функция Lookup


В Helm 3.1 добавлена функция Lookup, которая позволяет запросить существующий деплой и:

  • проверить существование ресурсов;
  • вернуть значение существующего ресурса для последующего использования.

Используя обе эти возможности, мы можем создать одноразовый динамически сгенерированный секрет!

# 1. Запросить существование секрета и вернуть в переменной $secret{{- $secret := (lookup "v1" "Secret" .Release.Namespace "some-awesome-secret" -}}apiVersion: v1kind: Secretmetadata:  name: some-awesome-secrettype: Opaque# 2. Если секрет существует, взять его значение как apiKey (секрет использует кодирование Base64, так что используйте ключ "data"){{ if $secret -}}data:  apiKey: {{ $secret.data.apiKey }}# 3. Если секрет не существует  создать его (в этот раз используйте "stringData", так как будет обычное значение)!{{ else -}}stringData:  apiKey: {{ uuidv4 | quote }}{{ end }}

Всякий раз, когда новое обновление применяется к серверу, Helm будет либо генерировать новое значение секрета (если секрета еще нет), либо повторно использовать существующее значение.

Успехов!

Что еще почитать по теме:

  1. Три уровня автомасштабирования в Kubernetes и как их эффективно использовать.
  2. Kubernetes в духе пиратства с шаблоном по внедрению.
  3. Наш канал Вокруг Kubernetes в Телеграме.
Подробнее..

Перевод Canary Deployment в Kubernetes 1 Gitlab CI

29.07.2020 16:12:53 | Автор: admin

Мы будем использовать Gitlab CI и ручной GitOps для внедрения и использования Canary-деплоя в Kubernetes





Статьи из этого цикла:


  • (эта статья)
  • Canary Deployment при помощи ArgoCI
  • Canary Deployment при помощи Istio
  • Canary Deployment при помощи Jenkins-X Istio Flagger

Выполнять Canary-деплой мы будем руками через GitOps и создание/изменение основных ресурсов Kubernetes. Эта статья предназначена в первую очередь для знакомства с тем, как работает в Kubernetes Canary деплой, так как есть более эффективные способы автоматизации, которые мы рассмотрим в следующих статьях.



https://www.norberteder.com/canary-deployment/


Canary Deployment


При Canary-стратегии обновления сначала применяются только для части пользователей. Через мониторинг, данные с логов, ручное тестирование или другие каналы обратной связи релиз тестируется перед его применением для всех пользователей.


Kubernetes Deployment (rolling update)


Стратегия по умолчанию для Kubernetes Deployment это rolling-update, где запускается определенное количество подов с новыми версиями образов. Если они создались без проблем, поды со старыми версиями образов завершаются, а новые поды создаются параллельно.


GitOps


Мы используем GitOps в этом примере, так как мы:


  • используем Git как единый источник истины
  • используем Git Operations для сборки и деплоя (никаких команд, кроме git tag/merge не нужно)

Пример


Возьмем хорошую практику иметь один репозиторий для кода приложений и один для инфраструктуры.


Репозиторий для приложений


Это очень простая API на Python+Flask, возвращающая ответ в виде JSON. Мы соберем пакет через GitlabCI и запушим результат в Gitlab Registry. В регистри у нас есть две разные версии релизов:


  • wuestkamp/k8s-deployment-example-app:v1
  • wuestkamp/k8s-deployment-example-app:v2

Единственная разница между ними это изменение возвращаемого JSON-файла. Мы используем это приложение для максимально простой визуализации того, с какой версией мы общаемся.


Инфраструктурный репозиторий


В этой репе мы будем деплоить через GitlabCI в Kubernetes, .gitlab-ci.yml выглядит следующим образом:


image: traherom/kustomize-dockerbefore_script:   - printenv   - kubectl versionstages: - deploydeploy test:   stage: deploy   before_script:     - echo $KUBECONFIG   script:     - kubectl get all     - kubectl apply -f i/k8s   only:     - master

Для его запуска самостоятельно вам понадобится кластер, можно использовать Gcloud:


gcloud container clusters create canary --num-nodes 3 --zone europe-west3-bgcloud compute firewall-rules create incoming-80 --allow tcp:80

Вам нужно сделать форк https://gitlab.com/wuestkamp/k8s-deployment-example-canary-infrastructure и создать переменную KUBECONFIG в GitlabCI, которая будет содержать конфиг для доступа kubectl к вашему кластеру.


О том, как получить учетные данные для кластера (Gcloud) можно почитать вот тут.


Инфраструктурный Yaml


В инфраструктурном репозитории у нас есть service:


apiVersion: v1kind: Servicemetadata: labels:   id: app name: appspec: ports: - port: 80   protocol: TCP   targetPort: 5000 selector:   id: app type: LoadBalancer

И deployment в deploy.yaml:


apiVersion: apps/v1kind: Deploymentmetadata: name: appspec: replicas: 10 selector:   matchLabels:     id: app     type: main template:   metadata:     labels:       id: app       type: main   spec:     containers:     - image: registry.gitlab.com/wuestkamp/k8s-deployment-example-app:v1       name: app       resources:         limits:           cpu: 100m           memory: 100Mi

И другой deployment в deploy-canary.yaml:


kind: Deploymentmetadata: name: app-canaryspec: replicas: 0 selector:   matchLabels:     id: app     type: canary template:   metadata:     labels:       id: app       type: canary   spec:     containers:     - image: registry.gitlab.com/wuestkamp/k8s-deployment-example-app:v2       name: app       resources:         limits:           cpu: 100m           memory: 100Mi

Заметьте, что app-deploy пока не имеет определенных реплик.


Выполнение начального деплоя


Для запуска начального deployment вы можете запустить пайплайн GitlabCI вручную в мастер-ветке. После этого kubectl дожен вывести следующее:





Мы видим app deployment c 10 репликами и app-canary с 0. Так же есть LoadBalancer с которого мы можем обращаться через curl по External IP:


while true; do curl -s 35.198.149.232 | grep label; sleep 0.1; done





Мы видим, что наше тестовое приложение возвращает только v1.


Выполнение Canary деплоя


Шаг 1: выпустить новую версию для части пользователей


Мы установили количество реплик в 1 в файле deploy-canary.yaml и образ новой версии:


kind: Deploymentmetadata: name: app-canaryspec: replicas: 1 selector:   matchLabels:     id: app     type: canary template:   metadata:     labels:       id: app       type: canary   spec:     containers:     - image: registry.gitlab.com/wuestkamp/k8s-deployment-example-app:v2       name: app       resources:         limits:           cpu: 100m           memory: 100Mi

В файле deploy.yaml мы изменили количество реплик до 9:


kind: Deploymentmetadata: name: appspec: replicas: 9 selector:   matchLabels:     id: app...

Мы пушим эти изменения в репозиторий, из которого запустится деплой (через GitlabCI) и видим в итоге:





Наш Service будет указывать на оба деплоя, так как у обоих есть селектор app. Из-за случайного распределения по умолчанию в Kubernetes мы должны увидеть разные ответы на ~ 10% запросов:





Текущее состояние нашего приложения (GitOps, взятый с Git как с Single Source Of Truth) это наличие двух deployments c активными репликами, по одному для каждой версии.


~10% пользователей знакомятся с новой версией и ненамеренно тестируют ее. Теперь настало время проверить наличие ошибок в логах и данных мониторинга для поиска проблем.


Шаг 2: выпустить новую версию для всех пользователей


Мы решили, что все прошло хорошо и теперь нам нужно развернуть новую версию на всех пользователей. Для этого мы просто обновляем deploy.yaml устанавливая новую версию образа и количество реплик, равное 10. В deploy-canary.yaml мы устанавливаем количество реплик равное обратно 0. После деплоя результат будет следующим:





Подводя итог


Для меня запуск деплоя вручную таким образом помогает понять как легко он может быть настроен при помощи k8s. Так как Kubernetes позволяет обновлять все через API, эти шаги можно автоматизировать через скрипты.


Еще одна вещь, которую нужно реализовать это точка входа тестировщика (LoadBalancer или через Ingress), через которую можно получить доступ только к новой версии. Она может быть использована для просмотра вручную.


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


Также читайте другие статьи в нашем блоге:


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru