Linux: настройка LEMP — NGINX, PHP, MySQL, SSL, мониторинг, логи, и миграция RTFM

Автор: | 11/05/2020
 

Собрался наконец-то перенести RTFM на Debian 10, решил делать без автоматизации — будем поднимать стандартный LEMP для хостинга WordPress руками.

Что-то похожее последний раз писалось в 2016 — Debian: установка LEMP — NGINX + PHP-FPM + MariaDB, в этот раз получился более полный обзор процесса.

Также, когда-то делал автоматизацию для настройки сервера под RTFM, но последний раз она использовалась в 2018 (см. AWS: миграция RTFM 3.0 (final) — CloudFormation и Ansible роли), а теперь многое поменялось/добавилось, и переделывать заново смысла нет — та автоматизация делалась больше для того, что бы потрогать Ansible/CloudFormation и вообще автоматизацию, чем для реального применения.

И снова — планировался краткий пост чисто по установке NGINX + PHP + MySQL, а в результате получается длинност по сетапу полноценного сервера — и мониторинг, и логи, и вот это вот всё.

Что будем делать:

  • создадим дроплет в DigitalOcean
  • SSL — используем Let’s Encrypt
  • NGINX
  • PHP-FPM
  • MySQL (MariaDB) в роли базы данных
  • NGINX Amplify agent — мониторинг и алертинг (Grafana и Loki не прижились, т.к. жрут слишком много ресурсов, но сам сетап был интересный, см. Prometheus: мониторинг для RTFM — Grafana, Loki и promtail)
  • WordPress backup script — самописный скрипт на Python, см. Python: скрипт бекапа файлов и баз MySQL в AWS S3
  • Logz.io — сбор логов в ELK-стек
  • unattended-upgrades — автоапдейты Debian и пакетов
  • logrotate — ротация логов

DigitalOceal: создание дроплета

Переехал на DigitalOcean из AWS кажется в прошлом году.

Причина — стоимость, так как DigitalOcean обходится намного дешевле. Работает в целом без нареканий, потому пока тут и остаюсь.

Создаём дроплет:

Будет Debian 10, 2 CPU, 2 GB RAM.

Например, на текущем сервере той же конфигурации нагрузка и потребление памяти выглядят так (график из NGINX Amplify):

Выбираем систему и тип  инстанса:

Выберем Франкфурт, и включим дополнительный мониторинг — на дроплете сразу будет запущен DigitalOcean-агент, который отрисует больше графиков в панели управления самого DO:

Создание RSA ключа для SSH

На рабочей машине создаём новую пару ключа:

ssh-keygen -f ~/Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/setevoy/Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11
Your public key has been saved in /home/setevoy/Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11.pub
...

Копируем содержимое публичной части:

cat /home/setevoy/Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11.pub
ssh-rsa AAAAB3NzaC1***Ht3UEYuGtdQgc0= setevoy@setevoy-arch-work

В DO создаём новый ключ:

Выбираем количество дроплетов 1, и задаём имя хоста rtfm-do-production-d10:

По желанию — включаем бекапы, и создаём дроплет:

Firewall

Пока создаётся дроплет — добавим для него фаервол:

Добавляем правила — SSH, ICMP — ограниченные только IP, на котором я сейчас, HTTP/S — отовсюду, хотя на время миграции имеет смысл ограничить доступ, что бы Google не проиндексировал блог, как копию оригинального сайта:

Подключаем к новому дроплету:

Floating IP

Аналог Elasitc IP в AWS, добавляем новый IP для нового сервера:

Тут, собственно, всё.

Теперь к серверу.

LEMP — Linux, NGINX, PHP, MySQL

Ещё раз — что нам надо будет насетапить на сервере?

  • nginx
  • php-fpm
  • lets encrypt
  • mysql
  • amplify agent — мониторинг
  • backup script
  • logz.io — есть free-доступ, правда логи хранит только деень — но мне больше и не надо
  • unattended-upgrades — автоапдейты системы и установленных пакетов
  • logrotate — на Debian есть по дефолту, проверим его настройки
  • msmtp — для отправки почты

Подключаемся на хост:

chmod 400 Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11
ssh -i Dropbox/AWS/setevoy-do-nextcloud-production-d10-03-11 root@139.59.205.180
root@rtfm-do-production-d10:~#

Обновляем систему, перезагружаемся:

root@rtfm-do-production-d10:~# apt update && apt -y upgrade

Устанавливаем пакеты для LEMP:

root@rtfm-do-production-d10:~# apt -y install certbot nginx php php-xml php-curl php-gd php-zip php-mysql php-mbstring php-fpm mariadb-server

Проверяем NGINX:

root@rtfm-do-production-d10:~# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Устанавливаем прочие полезные пакеты:

root@rtfm-do-production-d10:/data# apt -y install htop git wget unzip unattended-upgrades apt-listchanges dnsutils telnet python-pip python-boto3 mailutils

С mailutils есть проблема при использовании mailx с msmtp, пришлось заменить на bsd-mailx, см. mailx и msmtp — отправка почты с сервера.

Let’s Encrypt SSL

Используем сертификат от Let’s Encrypt.

Let’s Encrypt DNS validation

Возникает вопрос по валидации домена при получении сертификата — ведь сейчас домен направлен на старый сервер, значит стандартный метод с валидацией через веб-сервер и .well-known директорию не подойдёт.

Что сделаем — используем DNS-валидацию при получении сертификата, а потом, когда уже будет сертификат и настроенный NGINX — перенастроим certbot на валидацию через веб-сервер, т.к. DNS-валидация не поддерживает (но это не точно) renew сертификатов.

Стопаем NGINX:

root@rtfm-do-production-d10:~# systemctl stop nginx

Получаем сертификат:

root@rtfm-do-production-d10:~# certbot certonly --preferred-challenges dns -d rtfm.co.ua --manual --email user@example.com --agree-tos
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for rtfm.co.ua
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.
Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.rtfm.co.ua with the following value:
ORWOP6KR4C3csx-ngoSWbqVAJuVo8kFDgV8AqNFUemg
Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue

На DNS домена добавляем запись:

Проверяем:

dig _acme-challenge.rtfm.co.ua TXT +short
"ORWOP6KR4C3csx-ngoSWbqVAJuVo8kFDgV8AqNFUemg"

Возвращаемся к серверу, жмём Enter — готово:

...
Press Enter to Continue
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/rtfm.co.ua/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/rtfm.co.ua/privkey.pem
Your cert will expire on 2021-02-01. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"

NGINX

Генерируем DF-ключ (см. ClientKeyExchange) для NGINX:

root@rtfm-do-production-d10:~# openssl dhparam -out /etc/nginx/dhparams.pem 2048

Удаляем конфиг default — у нас RTFM будет дефолтным виртуалхостом:

root@rtfm-do-production-d10:~# rm /etc/nginx/sites-enabled/default

Создаём конфиг виртуалхоста для РТФМ — /etc/nginx/conf.d/rtfm.co.ua.conf. Он есть на старом сервере, можно просто скопировать.

У меня тут достаточно длинный конфиг получился, переношу уже его уже лет с 5 разных серверов, не меняется ничего кроме настроек SSL.

Последний тест на https://www.ssllabs.com всё ещё отдаёт уровень А+, так что можно использовать.

Вообще, удобная штука генераторы конфигов, например https://www.serverion.com/nginx-config или SSL Configuration Generator от Mozilla.

В моём конфиге под WordPress дополнительно блокируется доступ к /wp-admin и wp-login.php:

server {

    listen 80 default_server;
    server_name rtfm.co.ua www.rtfm.co.ua;

    server_tokens off;
    return 301 https://rtfm.co.ua$request_uri;
}

server {

    listen 443 ssl default_server;
    server_name rtfm.co.ua;

    root /data/www/rtfm/rtfm.co.ua;

    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
    server_tokens off;

#    access_log /var/log/nginx/rtfm.co.ua-access.log main_ext;
    error_log /var/log/nginx/rtfm.co.ua-error.log warn;

    ssl_certificate /etc/letsencrypt/live/rtfm.co.ua/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rtfm.co.ua/privkey.pem;

    ssl_protocols TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_stapling on;
    ssl_stapling_verify on;

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        root /usr/share/nginx/html;
    }

    client_max_body_size 1024m;

    location ~ /\.ht {
        deny all;
    }

    location ~* \.(jpg|swf|jpeg|gif|png|css|js|ico)$ {
        root /data/www/rtfm/rtfm.co.ua;
        expires 24h;
    }

    location /wp-admin/admin-ajax.php {

    location ~ \.php$ {
        include /etc/nginx/fastcgi_params;
        fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }

    location /wp-admin/ {

        index index.php, index.html;

        auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm;
        auth_basic "Password-protected Area";

        # office
        allow 194.***.***.24/29;
        # home 397 LocalNet
        allow 31.***.***.117/32;
        # home 397 Lanet
        allow 176.***.***.237;
        deny all;

        location ~ \.php$ {
            include /etc/nginx/fastcgi_params;
            fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }

    location /wp-config.php {
        deny all;
    }

    location /.user.ini {
        deny all;
    }

    location /wp-login.php {

        auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm;
        auth_basic "Password-protected Area";

        # office
        allow 194.***.***.24/29;
        # home 397 LocalNet
        allow 31.***.***.117/32;
        # home 397 Lanet
        allow 176.***.***.237;
        deny all;

        location ~ \.php$ {
            include /etc/nginx/fastcgi_params;
            fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    }

    location /uploads/noindex {
        auth_basic_user_file /data/www/rtfm/.htpasswd_rtfm;
        auth_basic "Password-protected Area";
    }

    location = /favicon.ico {
        access_log     off;
        log_not_found  off;
    }

    location / {

        try_files $uri =404;

        index index.php;
        proxy_read_timeout 3000;

        rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml$ "/index.php?xml_sitemap=params=$2" last;
        rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.xml\.gz$ "/index.php?xml_sitemap=params=$2;zip=true" last;
        rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html$ "/index.php?xml_sitemap=params=$2;html=true" last;
        rewrite ^/sitemap(-+([a-zA-Z0-9_-]+))?\.html.gz$ "/index.php?xml_sitemap=params=$2;html=true;zip=true" last;

        if (!-f $request_filename){
            set $rule_1 1$rule_1;
        }

        if (!-d $request_filename){
            set $rule_1 2$rule_1;
        }

        if ($rule_1 = "21"){
            rewrite /. /index.php last;
       }
    }

    location ~ \.php$ {

        try_files $uri =404;

        proxy_read_timeout 3000;
        include /etc/nginx/fastcgi_params;
        fastcgi_pass unix:/var/run/rtfm.co.ua-php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location /nginx_status {
        stub_status on;
        access_log   off;
        allow 127.0.0.1;
        deny all;
    }
}

Проверяем, запускаем (выключали, пока SSL получали):

root@rtfm-do-production-d10:~# nginx -t && systemctl start nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Проверяем.

На рабочей машине добавляем в /etc/hosts:

139.59.205.180 rtfm.co.ua

Пробуем открыть:

Отлично — SSL есть, NGINX работает.

И быстро отключаем, потому что в этот момент пишу этот пост на старом сервере 🙂

PHP-FPM

Аналогично — копируем конфиг со старого сервера.

Используем FPM-пулы, каждый под своим юзером.

См. PHP-FPM: Process Manager — dynamic vs ondemand vs static.

Linux: non-login user

Добавляем non-login юзера:

root@rtfm-do-production-d10:~# adduser --system --no-create-home --group rtfm
Adding system user `rtfm' (UID 109) ...
Adding new group `rtfm' (GID 115) ...
Adding new user `rtfm' (UID 109) with group `rtfm' ...
Not creating home directory `/home/rtfm'.

Создаём конфиг /etc/php/7.3/fpm/pool.d/rtfm.co.ua.conf:

[rtfm.co.ua]

user = rtfm
group = rtfm

listen = /var/run/rtfm.co.ua-php-fpm.sock

listen.owner = www-data
listen.group = www-data

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
;pm.process_idle_timeout = 10s;
;pm.max_requests = 500
catch_workers_output = yes
chdir = /
pm.status_path = /status
 
slowlog = /var/log/nginx/rtfm.co.ua-slow.log
php_flag[display_errors] = off
;php_admin_value[display_errors] = 'stderr'
php_admin_value[display_errors] = off
php_admin_value[error_log] = /var/log/nginx/rtfm.co.ua-php-error.log
php_admin_flag[log_errors] = on
php_admin_value[session.save_path] = /var/lib/php/session/rtfm
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session
php_admin_value[upload_max_filesize] = 128M
php_admin_value[post_max_size] = 128M

Проверяем синтаксис:

root@rtfm-do-production-d10:~# php-fpm7.3 -t
[03-Nov-2020 11:45:24] NOTICE: configuration file /etc/php/7.3/fpm/php-fpm.conf test is successful

Перечитываем конфиги:

root@rtfm-do-production-d10:~# systemctl reload php7.3-fpm.service

Рут в NIGNX у нас:

...
root /data/www/rtfm/rtfm.co.ua;
...

Создаём каталог:

root@rtfm-do-production-d10:~# mkdir -p /data/www/rtfm/rtfm.co.ua

Создём тестовый файл с вызовом phpinfo(), что бы проверить работу NGINX + PHP:

root@rtfm-do-production-d10:~# echo "<?php phpinfo(); ?>" > /data/www/rtfm/rtfm.co.ua/info.php

Проверяем (снова через обновление локального /etc/hosts):

Отлично — всё работает.

MySQL

На Debian уже достаточно давно используется MariaDB вместо MySQL, но разницы особой нет. И в целом MariaDB показывает себя в тестах лучше, где-то делал сравнение.

Запускаем первичную настройку:

root@rtfm-do-production-d10:~# mysql_secure_installation
...
Set root password? [Y/n] y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
... Success!
...
Remove anonymous users? [Y/n] y
... Success!
...
Disallow root login remotely? [Y/n] y
... Success!
...
Remove test database and access to it? [Y/n] y
- Dropping test database...
... Success!
- Removing privileges on test database...
... Success!
...
Reload privilege tables now? [Y/n] y
... Success!
Cleaning up...
All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.
Thanks for using MariaDB!

Создаём базу для RTFM:

MariaDB [(none)]> create database rtfm_db1_production;
Query OK, 1 row affected (0.000 sec)

Пользователя rtfm с доступом к базе rtfm_db1_production только с localhost с паролем password:

MariaDB [(none)]> GRANT ALL PRIVILEGES ON rtfm_db1_production.* TO 'rtfm'@'localhost' IDENTIFIED BY 'password';
Query OK, 0 rows affected (0.001 sec)

Проверяем:

root@rtfm-do-production-d10:~# mysql -u rtfm -p -e 'show databases;'
Enter password:
+--------------------+
| Database           |
+--------------------+
| information_schema |
| rtfm_db1_production|
+--------------------+

Всё готово к миграции.

Миграция блога

Тут мне придётся сделать паузу на время создание дампа и перенос файлов.

После переноса — продолжу писать уже с нового сервера.

Что делаем:

  1. создаём архив файлов
  2. создаём дамп базы
  3. переносим их на новый сервер
  4. меняем запись на DNS

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

Архив файлов

Готовим архив с файлами:

root@rtfm-do-production:/home/setevoy# cd /data/www/rtfm/
root@rtfm-do-production:/data/www/rtfm# ll
total 20
drwxr-xr-x 8 rtfm rtfm 20480 Nov 3 12:11 rtfm.co.ua

Создаём архив со сжатием:

root@rtfm-do-production:/data/www/rtfm# tar cvpfz rtfm.co.ua.tar.gz rtfm.co.ua/

Проверяем:

root@rtfm-do-production:/data/www/rtfm# ls -lh
total 2.4G
drwxr-xr-x 8 rtfm rtfm 20K Nov 3 12:11 rtfm.co.ua
-rw-r--r-- 1 root root 2.4G Nov 3 14:05 rtfm.co.ua.tar.gz

Дамп базы MySQL

Создаём дамп базы (прочтите сначала WordPress: Error establishing a database connection про опцию -d) :

root@rtfm-do-production:/data/www/rtfm# mysqldump -u rtfm -p -d rtfm_db1_production > rtfm_db1_production.sql
Enter password:

Проверяем его:

root@rtfm-do-production:/data/www/rtfm# head rtfm_db1_production.sql 
-- MySQL dump 10.16 Distrib 10.1.47-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost   Database: rtfm_db1_production
-- ------------------------------------------------------
-- Server version      10.1.47-MariaDB-0+deb9u1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

В фаерволе старого сервера разрешаем SSH с нового, и копируем данные со старого на новый:

root@rtfm-do-production-d10:/data# scp -i /root/.ssh/rtfm-do-old setevoy@67.207.75.202:/data/www/rtfm/rtfm.co.ua.tar.gz .
setevoy@67.207.75.202's password: 
rtfm.co.ua.tar.gz                                                                                                                                                                                         100% 2409MB 101.3MB/s  00:23    
root@rtfm-do-production-d10:/data# scp -i /root/.ssh/rtfm-do-old setevoy@67.207.75.202:/data/www/rtfm/rtfm_db1_production.sql .
setevoy@67.207.75.202's password: 
rtfm_db1_production.sql

Распаковываем файлы:

root@rtfm-do-production-d10:/data# tar xfpzv rtfm.co.ua.tar.gz

Проверяем:

root@rtfm-do-production-d10:/data# ll
total 2466844
drwxr-xr-x 8 rtfm rtfm      4096 Nov 3 10:11 rtfm.co.ua
-rw-r--r-- 1 root root 2525973949 Nov 3 12:14 rtfm.co.ua.tar.gz
-rw-r--r-- 1 root root     59424 Nov 3 12:14 rtfm_db1_production.sql
drwxr-xr-x 3 root root      4096 Nov 3 11:47 www

Переносим каталог rtfm.co.ua в /data/www/rtfm:

root@rtfm-do-production-d10:/data# rm -rf www/rtfm/rtfm.co.ua/
root@rtfm-do-production-d10:/data# mv rtfm.co.ua www/rtfm/

Проверяем:

root@rtfm-do-production-d10:/data# ll www/rtfm/rtfm.co.ua/
total 388
-rw-r--r-- 1 rtfm rtfm   64 Nov 6 2018 1a24c4e2948b4047d3d1ed8516b5ca39e452ccfdb2f81a46a8984b921261bd1e.txt
-rw-r--r-- 1 rtfm rtfm   24 Nov 6 2018 404.html
-rw-r--r-- 1 root root   58 Jul 25 2019 ads.txt
-rw-r--r-- 1 rtfm rtfm 28522 Nov 6 2018 bin_dec.html
-rw-r--r-- 1 rtfm rtfm 30682 Nov 6 2018 favicon.ico
-rw-r--r-- 1 rtfm rtfm  405 Apr 1 2020 index.php
-rw-r--r-- 1 rtfm rtfm 3080 Nov 6 2018 keybase.txt
-rw-r--r-- 1 rtfm rtfm 19915 Aug 12 07:46 license.txt
-rw-r--r-- 1 rtfm rtfm   20 Nov 6 2018 live-4d939769.tx
...

Загружаем дамп базы в новую базу:

root@rtfm-do-production-d10:/data# mysql -u rtfm -p rtfm_db1_production < rtfm_db1_production.sql 
Enter password:

Обновляем локальный /etc/hosts — и:

WTF?

WordPress: Error establishing a database connection

Проверяем данные в базе — вроде всё на месте:

MariaDB [rtfm_db1_production]> show tables;            
+--------------------------------+
| Tables_in_rtfm_db1_production |
+--------------------------------+
| b2s_posts                     |
| b2s_posts_network_details     |
| b2s_posts_sched_details       |
| b2s_user                      |
| b2s_user_contact              |
| b2s_user_network_settings     |
...

PHP: проверить подключение к MySQL

Пробуем РНР-скрипт:

<?php 
 
$link = @mysqli_connect('localhost', 'rtfm', 'Ta6paidie7Ie'); 
 
if(!$link) { 
   die("Failed to connect to the server: " . mysqli_connect_error()); 
} else { 
   echo "Connected\n"; 
} 
 
 
if(!@mysqli_select_db($link, 'rtfm_db1_production')) { 
   die("Failed to connect to the database: " . mysqli_error($link)); 
} else { 
   echo "DB found\n"; 
} 
 
?>

Запускаем:

root@rtfm-do-production-d10:/data/www/rtfm/rtfm.co.ua# php mysql.php 
Connected
DB found

Тоже всё хорошо.

WordPress: WP_ALLOW_REPAIR

Копируем со старого сервера .htpasswd_rtfm, пробуем выполнить repair — в wp-config.php перед строкой «‘That’s all, stop editing! Happy blogging’» добавлем:

define('WP_ALLOW_REPAIR', true);

И открываем https://rtfm.co.ua/wp-admin/maint/repair.php:

Вроде бы всё ОК — но нет:

Последним вариантом проверки было засетапить голый WordPress, который завёлся без проблем.

Понятно было, что проблема в данных в самом дампе — но какая?

Причина «Error establishing a database connection»

Пошёл я посмотреть опции mysqldump, и тут меня осенило:

-d--no-data Do not write any table row information (that is, do not dump table contents). This is useful if you want to dump only the CREATE TABLE statement for the table (for example, to create an empty copy of the table by loading the dump file). See also —ignore-table-data .

😀

С какого перепугу я добавил -d при создании дампа — не знаю. Аукнулась работа с миграцией даных в AWS Database Migration Service, где надо было создать дамп чисто схемы, без самих данных.

Собственно — пересоздаём дамп уже без -d:

root@rtfm-do-production:/home/setevoy# mysqldump -u rtfm -p rtfm_db1_production > rtfm_db1_production.sql
Enter password:

Повторяем все операции, и всё завелось — продолжаю писать уже с нового сервера:

13:59:52 [setevoy@setevoy-arch-work ~]  $ dig rtfm.co.ua +short
139.59.205.180

Что дальше?

Надо настроить certbot на валидацию через webroot, добавить его в крон для автообновления сертификатов.

И закончить со оставшимися сервисами:

  • amplify agent
  • backup script
  • logz.io
  • unattended-upgrades
  • msmtp

SSL: webroot validation

У нас уже есть сертификат, но мы делали валидацию через DNS.

Она вроде не работает при renew, поэтому перенастроим на валидацию через webroot.

Let’s Encrypt: webroot валидация

Вызываем certbot для домена rtfm.co.ua, передаём --webroot-path вместо dns — он должен увидеть имеющийся сертификат, и предложить сохранить, или сгенерировать новый.

На первый вопрос «How would you like to authenticate with the ACME CA?» отвечаем «Place files in webroot directory (webroot)«, на второй — «You have an existing certificate […]» — «Renew & replace the cert (limit ~5 per 7 days)«, что бы сгенерировался новый конфиг для Let’s Ecnrypt:

root@rtfm-do-production-d10:/data# certbot certonly -d rtfm.co.ua --email user@example.com --agree-tos --webroot-path /data/www/rtfm/rtfm.co.ua/.well-known/
Saving debug log to /var/log/letsencrypt/letsencrypt.log
How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Spin up a temporary webserver (standalone)
2: Place files in webroot directory (webroot)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Plugins selected: Authenticator webroot, Installer None
Cert not yet due for renewal
You have an existing certificate that has exactly the same domains or certificate name you requested and isn't close to expiry.
(ref: /etc/letsencrypt/renewal/rtfm.co.ua.conf)
What would you like to do?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Keep the existing certificate for now
2: Renew & replace the cert (limit ~5 per 7 days)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Renewing an existing certificate
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/rtfm.co.ua/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/rtfm.co.ua/privkey.pem
...

Да, всё хорошо — теперь проверим конфиг, который будет использоваться при обновлении сертификата:

root@rtfm-do-production-d10:/data# cat /etc/letsencrypt/renewal/rtfm.co.ua.conf
renew_before_expiry = 30 days
version = 0.31.0
archive_dir = /etc/letsencrypt/archive/rtfm.co.ua
cert = /etc/letsencrypt/live/rtfm.co.ua/cert.pem
privkey = /etc/letsencrypt/live/rtfm.co.ua/privkey.pem
chain = /etc/letsencrypt/live/rtfm.co.ua/chain.pem
fullchain = /etc/letsencrypt/live/rtfm.co.ua/fullchain.pem
Options used in the renewal process
[renewalparams]
account = 868c8164304408984fefbbff845d4f48
authenticator = webroot
server = https://acme-v02.api.letsencrypt.org/directory
webroot_path = /data/www/rtfm/rtfm.co.ua/.well-known,
[[webroot_map]]

Отлично — можно добавлять cronjob.

certbot renew — автообновление

Добавляем в cron задачу — раз в неделю выполнять certbot renew.

Редактируем crontab:

root@rtfm-do-production-d10:/data# crontab -e

Добавляем:

@weekly certbot renew &> /var/log/letsencrypt/letsencrypt.log

Let’s Encrypt hook — NGINX reload

Последний шаг — релоадить NGINX после обновления сертификата.

Можно было ,s добавить в cron, например так:

@weekly certbot renew &> /var/log/letsencrypt/letsencrypt.log && service nginx reload

Но тогда если один из сертификатов не обновится по какой-то причине — NGINX не применит изменения вообще.

Поэтому делаем более правильно — добавляем хук в конфиг нашего домена — /etc/letsencrypt/renewal/rtfm.co.ua.conf.

В блоке renewalparams добавляем renew_hook, теперь он выглядит так:

[renewalparams]
account = 868c8164304408984fefbbff845d4f48
authenticator = webroot
server = https://acme-v02.api.letsencrypt.org/directory
webroot_path = /data/www/rtfm/rtfm.co.ua/.well-known,
renew_hook = systemctl reload nginx

Собственно, с SSL мы закончили.

Amplify — мониторинг

Достаточно базовый мониторинг — но удобен, и сетапится в несколько минут. См. NGINX: Amplify — SaaS мониторинг от NGINX.

Официальная документация — тут>>>.

Загружаем установочный скрипт:

Передаём API ключ в переменной и запускаем скрипт:

root@rtfm-do-production-d10:/tmp# API_KEY='967***e31' sh ./install.sh

Пару минут на установку — и новый хост появляется в дашборде:

Интереса ради — нагрузка на старом сервере после переключения rtfm.co.ua на новый сервер:

Backup скрипт для сайтов

Использую самописный скрипт, который делался ещё 3 года тому — https://github.com/setevoy2/simple-backup. Скрипт архивирует файлы, и создаёт дамп базы, плюс умеет загружать в AWS S3.

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

Клонируем его:

root@rtfm-do-production-d10:/tmp# cd /opt/
root@rtfm-do-production-d10:/opt# git clone https://github.com/setevoy2/simple-backup

Интересно — работает ли та копия, которая в репозиториии…

Точно помню, что когда-то поломалась загрузка архивов в AWS S3, и я её так и не починил. Может, в отпуске дойдут руки.

Пока пробуем, как есть:

root@rtfm-do-production-d10:/opt# python simple-backup/sitebackup.py -h
usage: sitebackup.py [-h] [-c CONFIG]
optional arguments:
-h, --help            show this help message and exit
-c CONFIG, --config CONFIG

Ну, вроде да, работает.

Для работы скрипта нужен каталог /backups, который монтируется отдельным диском, и файл настроек.

Добавим сначала диск.

Диски и разделы на сервере сейчас:

root@rtfm-do-production-d10:/opt# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
vda    254:0    0   60G  0 disk
├─vda1 254:1    0   60G  0 part /
└─vda2 254:2    0    2M  0 part
vdb    254:16   0  466K  1 disk

DigitalOcean Volume

Переходим в DigitalOcean, создаём Volume:

Проверяем:

root@rtfm-do-production-d10:/opt# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0   50G  0 disk /mnt/rtfm_do_production_d10_backups
vda    254:0    0   60G  0 disk
├─vda1 254:1    0   60G  0 part /
└─vda2 254:2    0    2M  0 part
vdb    254:16   0  466K  1 disk

Linux: монтирование диска

DigitalOcean Volume по-умолчанию монтируется в /mnt/rtfm_do_production_d10_backups, и не создаёт запись в fstab:

root@rtfm-do-production-d10:/opt# cat /etc/fstab
/etc/fstab: static file system information.
UUID=4e8b8101-6a06-429a-aaca-0ccd7ff14aa1       /       ext4    errors=remount-ro       0       1

Отмонтируем его:

root@rtfm-do-production-d10:/opt# umount /mnt/rtfm_do_production_d10_backups

Создаём каталог /backups:

root@rtfm-do-production-d10:/opt# mkdir /backups

Получаем UUID диска:

root@rtfm-do-production-d10:/opt# blkid /dev/sda
/dev/sda: UUID="a6e27193-4079-4d9d-812e-6ba29c702b75" TYPE="ext4"

Обновляем /etc/fstab — добавляем монтирование этого диска в /backups, в opts с помощью nofail указываем, что его монтирование не обязательно, что бы система могла загрузиться, если этого диска не будет:

# /etc/fstab: static file system information.
UUID=4e8b8101-6a06-429a-aaca-0ccd7ff14aa1   /   ext4    errors=remount-ro   0   1
UUID=a6e27193-4079-4d9d-812e-6ba29c702b75   /backups ext4 nofail 0 0

Пробуем смонтировать все разделы, указанные в /etc/fstab:

root@rtfm-do-production-d10:/opt# mount -a

Проверяем:

root@rtfm-do-production-d10:/opt# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0   50G  0 disk /backups
vda    254:0    0   60G  0 disk
├─vda1 254:1    0   60G  0 part /
└─vda2 254:2    0    2M  0 part
vdb    254:16   0  466K  1 disk

Вроде всё ок, и данные на месте:

root@rtfm-do-production-d10:/opt# ll /backups/
total 16
drwx------ 2 root root 16384 Nov  4 12:44 lost+found

Ещё можно перезагрузить машину, что бы убедиться, что всё работает — но это попозже, когда допишу пост.

Со старого сервера копируем конфиг для simple-backup, и пробуем его запустить:

root@rtfm-do-production-d10:/opt# /opt/simple-backup/sitebackup.py -c /usr/local/etc/production-simple-backup.ini
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - found, OK.
/backups/files - found, OK.
/backups/databases - found, OK.
Creating WWW backup for:
site: rtfm
from: /data/www/rtfm/rtfm.co.ua/
to: /backups/files/04-11-2020-12-58_rtfm_rtfm.co.ua.gz
WWW backup done.
Creating DB backup for:
site: rtfm
host: localhost
database: rtfm_db1_production
user: rtfm
to: /backups/databases/04-11-2020-12-58_rtfm_rtfm_db1_production.sql
DB backup done.
Checking for dependencies:
boto3 library already installed - OK.
Uploading /backups/files/04-11-2020-12-58_rtfm_rtfm.co.ua.gz to S3 bucket setevoy-rtfm-simple-backups-production as 04-11-2020-12-58_rtfm_rtfm.co.ua.gz
Uploading /backups/databases/04-11-2020-12-58_rtfm_rtfm_db1_production.sql to S3 bucket setevoy-rtfm-simple-backups-production as 04-11-2020-12-58_rtfm_rtfm_db1_production.sql
Existing data in the setevoy-rtfm-simple-backups-production bucket:
04-11-2020-12-58_rtfm_rtfm.co.ua.gz
04-11-2020-12-58_rtfm_rtfm_db1_production.sql
...
Starting local backups storage cleanup...
Keeping local data: /backups/files/04-11-2020-12-52_rtfm_rtfm.co.ua.gz
Keeping local data: /backups/files/04-11-2020-12-58_rtfm_rtfm.co.ua.gz
Keeping local data: /backups/databases/04-11-2020-12-58_rtfm_rtfm_db1_production.sql
Keeping local data: /backups/databases/04-11-2020-12-52_rtfm_rtfm_db1_production.sql

Вау!

И даже загрузка в AWS S3 снова работает!

Круто. Окей — с этим тоже закончили.

Что там дальше?

  • logz.io
  • unattended-upgrades
  • logrotate
  • msmtp

Logz.io, Filebeat и логи NGINX

Добавим сбор логов NGINX и отправку в Logz.io.

Регистрируем аккаунт, переходим к инструкции по сбору логов NGINX — https://app.logz.io/#/dashboard/data-sources/nginx.

Надо установить Filebeat, используем 7, устанавливаем его:

root@rtfm-do-production-d10:/opt# cd /tmp/
root@rtfm-do-production-d10:/tmp# curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.9.3-amd64.deb
root@rtfm-do-production-d10:/tmp# dpkg -i filebeat-7.9.3-amd64.deb

Получаем публичный сертификат для Logz.io:

root@rtfm-do-production-d10:/tmp# sudo curl https://raw.githubusercontent.com/logzio/public-certificates/master/AAACertificateServices.crt --create-dirs -o /etc/pki/tls/certs/COMODORSADomainValidationSecureServerCA.crt

Настраиваем Filebeat.

Бекапим конфиг:

root@rtfm-do-production-d10:/tmp# cp /etc/filebeat/filebeat.yml /etc/filebeat/filebeat.yml-origin

Редактируем его, обновляем, как сказано в документации:

...
- type: log
  paths:
  - /var/log/nginx/access.log
  - /var/log/nginx/rtfm.co.ua-access.log
  fields:
    logzio_codec: plain
    token: JzR***ZmW
    type: nginx_access
  fields_under_root: true
  encoding: utf-8
  ignore_older: 3h
- type: log
  paths:
  - /var/log/nginx/error.log
  - /var/log/nginx/rtfm.co.ua-error.log
  fields:
    logzio_codec: plain
    token: JzR***ZmW
    type: nginx_error
  fields_under_root: true
  encoding: utf-8
  ignore_older: 3h

...

И outputs комментируем output.elasticsearch, и описываем output.logstash, как сказано в документации:

...
# ------------------------------ Logstash Output -------------------------------
# ...
output.logstash:
  hosts: ["listener.logz.io:5015"]
  ssl:
    certificate_authorities: ['/etc/pki/tls/certs/COMODORSADomainValidationSecureServerCA.crt'
...

Проверяем синтаксис конфига:

root@rtfm-do-production-d10:/tmp# filebeat test config
Config OK

И подключение к Logz.io:

root@rtfm-do-production-d10:/tmp# filebeat test output
logstash: listener.logz.io:5015...
connection...
parse host... OK
dns lookup... OK
addresses: 23.22.183.192
dial up... OK
TLS...
security: server's certificate chain verification is enabled
handshake... OK
TLS version: TLSv1.2
dial up... OK
talk to server... OK

Перезапускаем его:

root@rtfm-do-production-d10:/tmp# systemctl restart filebeat

Проверяем логи:

Данные пошли.

Остались unattended-upgrades и logrotate.

Установка unattended-upgrades

Описывалось в Debian: автоматические обновления с помощью unattended-upgrades и отправка почты через AWS SES, настроим на новом сервере, только без AWS SES, наверно.

Официальная документация — тут>>>.

unattended-upgrades и apt-listchanges мы уже установили, осталось настроить обновления.

Вызываем dpkg-reconfigure unattended-upgrades:

Отвечаем Yes.

Проверим /etc/apt/apt.conf.d/20auto-upgrades:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

APT::Periodic::Enable для включения апдейтов уже не требуется, достаточно этих двух строк.

Проверим /etc/apt/apt.conf.d/50unattended-upgrades.

В целом, там всё можно оставить по-умолчанию, но стоит добавить:

  • Unattended-Upgrade::Mail — получать письма об установленных апдейтах
  • Unattended-Upgrade::Automatic-Reboot — на своё усмотрение, пока можно оставить в false, включить позже
  • Unattended-Upgrade::Automatic-Reboot-Time — если включаем автоматическую перезагрузку — лучше ребутаться под утро, часов в 5-6 — я просыпаюсь в 7-8, если что — с утра сразу увижу, что сервер не поднялся

Запускаем пробный апгрейд:

root@rtfm-do-production-d10:/tmp# unattended-upgrade -v -d --dry-run
...
No packages found that can be upgraded unattended and no pending auto-removals

Окей.

Теперь глянем настройки logrotate.

logrotate

Собственно, тут уже всё настроено до нас, но проверить надо.

Все конфиги logrotate:

root@rtfm-do-production-d10:/tmp# ll /etc/logrotate.d/
total 60
-rw-r--r-- 1 root root 120 Apr 19  2019 alternatives
-rw-r--r-- 1 root root 122 Sep 23  2019 amplify-agent
-rw-r--r-- 1 root root 173 May 12 09:57 apt
-rw-r--r-- 1 root root  79 Feb 13  2019 aptitude
-rw-r--r-- 1 root root 130 Aug 28  2018 btmp
-rw-r--r-- 1 root root  82 May 26  2018 certbot
-rw-r--r-- 1 root root 112 Apr 19  2019 dpkg
-rw-r--r-- 1 root root 146 May 13 16:01 exim4-base
-rw-r--r-- 1 root root 126 May 13 16:01 exim4-paniclog
-rw-r--r-- 1 root root 802 Oct 12 17:46 mysql-server
-rw-r--r-- 1 root root 329 Aug 24 10:18 nginx
-rw-r--r-- 1 root root 155 Jul  5 06:46 php7.3-fpm
-rw-r--r-- 1 root root 501 Feb 26  2019 rsyslog
-rw-r--r-- 1 root root 235 Jun  8  2019 unattended-upgrades
-rw-r--r-- 1 root root 145 Feb 19  2018 wtmp

Ротация логов NGINX:

root@rtfm-do-production-d10:/tmp# cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 0640 www-data adm
sharedscripts
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi \
endscript
postrotate
invoke-rc.d nginx rotate >/dev/null 2>&1
endscript
}

Возможно, тут надо будет добавить параметр size.

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

root@rtfm-do-production-d10:/tmp# logrotate -f -v /etc/logrotate.conf
...
considering log /var/log/kern.log
Now: 2020-11-04 14:25
Last rotated at 2020-11-04 00:00
log needs rotating
...

Некоторые логи уже должны будут ротейтнуться, но для NGINX пока рано.

mailx и msmtp — отправка почты с сервера

Пользователю root будут сыпаться всякие служебные письма — хотелось бы их получать на свой почтовый ящик.

Для начала — проверяем /etc/aliases:

root@rtfm-do-production-d10:/tmp# cat /etc/aliases
/etc/aliases
mailer-daemon: postmaster
postmaster: root
nobody: root
hostmaster: root
usenet: root
news: root
webmaster: root
www: root
ftp: root
abuse: root
noc: root
security: root
root: root@example.com

Если делали изменения в этом конфиге — выполняем:

root@rtfm-do-production-d10:/tmp# newaliases

550 001.RDNS/PTR error. Rejected

Письма для root пойдут на root@example.com, но если сейчас на него отправить почту — письмо не уйдёт:

root@rtfm-do-production-d10:/tmp# echo Test | mailx -s Test root@example.com

Потому что идёт через MTA Exim. Его лог:

root@rtfm-do-production-d10:/tmp# tail /var/log/exim4/mainlog
...
2020-11-04 14:38:16 1kaJvU-00032w-7q <= root@rtfm-do-production-d10 U=root P=local S=405
...
2020-11-04 14:39:08 1kaJvI-00032T-Dx ** root@example.com <root@rtfm-do-production-d10> R=dnslookup T=remote_smtp H=mx1.mail7.freehost.com.ua [194.0.200.210] X=TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256 CV=no DN="CN=*.freehost.com.ua": SMTP error from remote mail server after RCPT TO:<root@example.com>: 550 001.RDNS/PTR error. Rejected

«550 001.RDNS/PTR error. Rejected» — потому что не настроена PTR-запись для FloatingIP, и у DigitalOcean мы сами нормально ей управлять не можем.

Потому — ставим msmtp, что бы слать через внешний SMTP-сервер, а не локальный:

root@rtfm-do-production-d10:/tmp# apt -y install msmtp msmtp-mta

Раньше был ssmtp, он уже депрекейтнулся, потому используем msmtp.

msmtp-mta создаст симлинк /usr/sbin/sendmail — когда mailx будет пытаться отправить почту через sendmail, который является дефолтным MTA — он использует msmtp:

root@rtfm-do-production-d10:/tmp# ls -l /usr/sbin/sendmail
lrwxrwxrwx 1 root root 12 Feb 15  2019 /usr/sbin/sendmail -> ../bin/msmtp

Настраиваем /etc/msmtprc:

defaults
port 25
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt

account freehost
host freemail.freehost.com.ua
from user@example.com
auth on
user user@example.com
password password

# Set a default account
account default : freehost

Проверяем:

root@rtfm-do-production-d10:/tmp# echo "test username." | msmtp -a default myuser@google.com

mailx: cannot send message: process exited with a non-zero status

Что бы отправлять почту через mailx с помощью msmtp — устанавливаем bsd-mailx вместо mailutils:

root@rtfm-do-production-d10:/tmp# apt -y purge mailutils
root@rtfm-do-production-d10:/tmp# apt -y install bsd-mailx

Иначе будем получать ошибки «mailx: cannot send message: process exited with a non-zero status» и   «msmtp: no recipients found«.

Пробуем отправку с mailx:

root@rtfm-do-production-d10:/tmp# echo Test | mailx -s Test myuser@google.com

Теперь письма от unattended-upgrades должны приходить на ящик, заданный в Unattended-Upgrade::Mail.

В целом — на этом вроде бы всё.