С CentOS 7 на AlmaLinux 9: два честных пути
Поддержка CentOS 7 закончилась 30 июня 2024 года. В то утро ничего не произошло, сервер продолжил отдавать трафик, и в этом вся проблема. Сегодняшняя машина на CentOS 7 грузит ядро 3.10, собрана с glibc 2.17 и OpenSSL 1.0.2, и ни для чего из этого больше не выходят исправления. Ниже два пути, которые реально работают: новый сервер на AlmaLinux 9 с переносом данных, что мы рекомендуем для всего, что смотрит в интернет, и обновление на месте через ELevate для машин, которые действительно нельзя пересобрать.
Что на самом деле перестаёт работать
Очевидный ответ, обновления безопасности, верен, но на практике всё ломается с другой стороны.
- Пакеты становится трудно ставить. mirror.centos.org отключён. CentOS 7 остался только на vault.centos.org, замороженный на 7.9.2009. Всё, что вы оттуда поставите, ровно такой же давности, как день окончания поддержки.
- Современный софт отказывается работать. Актуальному Node.js, контейнерным движкам, агентам мониторинга и многим пакетам Composer и pip нужны более новые glibc, OpenSSL 3 или Python, которых в CentOS 7 нет.
- Чужие серверы начинают отказывать вам. Вот это прилетает без предупреждения. Платёжные шлюзы, банки и API партнёров отключают TLS 1.0 и 1.1 и старые шифры, и ваш cron внезапно не может подключиться. В OpenSSL 1.0.2 нет TLS 1.3 в принципе.
- Тихо умирает продление сертификатов. ACME-клиенты отказались от Python 2, поэтому нетронутый CentOS 7 рано или поздно перестаёт продлевать собственные сертификаты.
- Аудиты и опросники. Любая проверка безопасности со стороны клиента, оценка PCI или анкета страховщика теперь называет эту ОС отдельным пунктом.
Сначала опишите, что вообще крутится на старой машине
Большинство провальных миграций проваливаются потому, что на старом сервере работало что-то, о чём никто не помнил. Потратьте час на инвентаризацию. На машине с CentOS 7:
rpm -qa --qf '%{NAME} %{VERSION} %{VENDOR}\n' | sort -k3 > /root/inventory-packages.txt
systemctl list-unit-files --state=enabled > /root/inventory-services.txt
ss -tulpn > /root/inventory-ports.txt
iptables-save > /root/inventory-iptables.rules
crontab -l; ls -l /etc/cron.d /etc/cron.daily
for u in $(getent passwd | awk -F: '$3>=1000 {print $1}'); do crontab -l -u $u; done
httpd -M; php -v; php -m
mysql -e "SELECT VERSION(); SHOW DATABASES;"
Сортировка пакетов по вендору даёт больше всего пользы: она сразу показывает всё, что пришло из EPEL, Remi, IUS или Webtatic, а именно у этого софта нет автоматического аналога в AlmaLinux 9. Затем найдите файлы, которые не принадлежат ни одному пакету, и конфиги, правленные руками:
rpm -Va --nomtime --nosize | grep '^..5' | grep ' c '
find /opt /usr/local /srv -maxdepth 3 -type f | head -100
Если на старой машине для миграции всё же надо что-то доустановить, переключите yum на vault. Он заморожен, поэтому используйте его только под миграцию:
cp -a /etc/yum.repos.d/CentOS-Base.repo /root/CentOS-Base.repo.bak
sed -i 's|^mirrorlist=|#mirrorlist=|' /etc/yum.repos.d/CentOS-Base.repo
sed -i 's|^#\?baseurl=http://mirror.centos.org/centos/$releasever|baseurl=http://vault.centos.org/7.9.2009|' /etc/yum.repos.d/CentOS-Base.repo
grep -E '^(mirrorlist|baseurl)' /etc/yum.repos.d/CentOS-Base.repo
yum clean all && yum makecache
grep здесь не для красоты. Если шаблон не совпадёт, замена молча не сработает и yum останется нацелен на хост, который больше не отвечает, поэтому проверьте до того, как идти дальше: каждый baseurl теперь должен начинаться с http://vault.centos.org/7.9.2009, а каждая строка mirrorlist быть закомментирована. Один проход закрывает base, updates, extras и centosplus: они лежат в одном файле. Любой другой репозиторий, EPEL или SCLo, либо переведите на его собственный архив, либо отключите на время работ.
Путь первый: новый сервер на AlmaLinux 9
Этот путь мы рекомендуем для сайтов, магазинов, API и всего, у чего впереди живые пользователи. Вы собираете чистую машину, переносите данные, тестируете, пока старый сервер ещё работает, а переключение сводится к записи DNS, которую можно вернуть назад. Конверсия на месте ничего из этого не даёт. Новую машину можно взять как VPS, если нагрузка позволяет, или как выделенный сервер, если упираетесь в процессор или диск. И то и другое работает в нашем собственном дата-центре в Риге и в нашей собственной сети, а если ваш нынешний сервер уже у нас, старая и новая машина на время работ остаются в одном дата-центре.
Сначала базовая настройка: учётные записи, SSH-ключи, часовой пояс, мониторинг, агент резервного копирования. Потом воспроизводите стек, а не машину. Цель в том, чтобы получить поддерживаемую конфигурацию AlmaLinux 9, на которой работает ваше приложение, а не побайтовую копию десятилетнего сервера.
Скачки версий, которые бьют больнее всего
| Компонент | CentOS 7 | AlmaLinux 9 | Что ломается |
|---|---|---|---|
| PHP | 5.4 в базе | 8.0 в AppStream, 8.1 и новее как потоки модулей | mod_php больше нет, удалённые функции, строгие сравнения |
| База данных | MySQL 5.7 или MariaDB 5.5 | MariaDB 10.11 как поток модуля, MySQL 8.0 обычным пакетом | строгость sql_mode, нулевые даты, плагины авторизации, сравнения строк |
| Python | 2.7 как /usr/bin/python | системный 3.9, Python 2 отсутствует | каждый скрипт со старой строкой shebang |
| TLS | OpenSSL 1.0.2 | OpenSSL 3 и системная криптополитика | TLS 1.0 и 1.1 выключены, подписи SHA-1 и RSA короче 2048 бит отвергаются |
| Файрвол | iptables | nftables и firewalld 1.x | сохранённые правила не загружаются, ipset и iptables-nft объявлены устаревшими |
| Настройка сети | ifcfg, network-scripts | keyfile NetworkManager | пакета network-scripts больше нет |
| SELinux | обычно выключен | enforcing, политика targeted | отказы, которые выглядят как баги приложения |
PHP
Сначала посмотрите, какие потоки есть именно в вашем минорном релизе: их добавляли на протяжении жизни AlmaLinux 9, а простой dnf install php ставит 8.0:
dnf module list php
dnf module enable php:8.2 -y
dnf install -y php php-fpm php-mysqlnd php-gd php-mbstring php-xml php-opcache
systemctl enable --now php-fpm
Структурное изменение, которого никто не ждёт: в EL8 и EL9 mod_php не существует. PHP работает через php-fpm, Apache общается с ним по сокету. Делать для этого ничего не нужно: пакеты PHP кладут /etc/httpd/conf.d/php.conf с глобальным блоком FilesMatch и SetHandler, поэтому PHP работает в каждом виртуальном хосте без правок. Обработчик вы пишете руками только тогда, когда хосту нужен собственный пул php-fpm:
SetHandler "proxy:unix:/run/php-fpm/app.sock|fcgi://localhost"
Ломается на самом деле обратное: остатки старой конфигурации, причём ломаются они двумя разными способами. Отдельная строка php_value или php_flag без mod_php становится неизвестной директивой: в виртуальном хосте или в httpd.conf Apache отказывается стартовать с Invalid command 'php_value', а в файле .htaccess отдаёт 500 только на этом пути, тогда как остальной сайт продолжает работать. Тихий отказ приходится искать самому: всё, что лежит внутри блока <IfModule mod_php5.c>, пропускается молча, потому что модуля нет, так что эти настройки просто теряются и в логе о них ни строки. AddHandler php5-script остаётся обычной директивой mod_mime и Apache не роняет, а глобальный SetHandler из php.conf применяется после .htaccess и побеждает, поэтому PHP работает, а строка просто лишняя. Откуда бы они ни взялись, сами настройки переезжают в файл пула в /etc/php-fpm.d/ как записи php_admin_value[...] либо в /etc/php.d/.
В коде, написанном под 5.4, будут вызовы mysql_*, each(), create_function() и обращение к символам строки через фигурные скобки, всё это удалено. PHP 8 ещё и поменял сравнение строки с числом, так что "abc" == 0 теперь false: старая валидация не падает с ошибкой, а тихо начинает вести себя иначе. Начните с проверки синтаксиса, дальше читайте код:
find /var/www -name '*.php' -print0 | xargs -0 -n1 -P4 php -l | grep -v 'No syntax errors'
Базы данных
Снимайте дамп с явно указанной кодировкой, и берите --lock-all-tables вместо --single-transaction, если хоть одна таблица ещё на MyISAM:
mysqldump --single-transaction --routines --triggers --events --default-character-set=utf8mb4 --databases app > /root/app.sql
На новой машине включите поток, поставьте сервер и импортируйте:
dnf module list mariadb
dnf module enable mariadb:10.11 -y
dnf install -y mariadb-server
systemctl enable --now mariadb
mysql < /root/app.sql
mariadb-upgrade
Если вы остаётесь на MySQL, а не на MariaDB, шаг с модулем пропустите совсем: в AlmaLinux 9 MySQL 8.0 поставляется обычным пакетом AppStream, потока mysql для включения нет, а вся установка сводится к dnf install -y mysql-server. Обратите внимание на имена: начиная с MariaDB 10.5 утилиты называются mariadb, mariadb-dump и mariadb-upgrade, а старый mysql_upgrade оставлен только символической ссылкой. На импорте стабильно спотыкаются о две вещи. Конструкции DEFINER во вьюхах и процедурах ссылаются на пользователей, которых ещё нет, поэтому создайте пользователей заранее или вырежьте эти конструкции. И даты вида 0000-00-00 отвергаются более строгим sql_mode по умолчанию, из-за чего рабочая вставка старого кода падает уже в бою, а не на импорте. Задайте character_set_server и sql_mode явно в файле в /etc/my.cnf.d/, не полагаясь на умолчания: они между версиями разные.
Python 2
В AlmaLinux 9 Python 2 нет вообще, даже модулем, и /usr/bin/python по умолчанию не существует. Любой внутренний скрипт с #!/usr/bin/python падает сразу. Мелкие скрипты обычно быстрее переписать. В EL8 пакет python2 ещё был, поэтому остановка на AlmaLinux 8 даёт интерпретатор, который хотя бы ставится, но и этот поток закончил жизненный цикл в июне 2024 года и исправлений тоже не получает, а на 9 ставить уже нечего вообще. Код, который переписать действительно нельзя, придётся запускать в контейнере на более старом базовом образе либо держать на старой машине, пока его не перепишут. Не собирайте Python 2 из исходников на боевом сервере. Отдельный случай, на который как раз отвечает python3.11: приложению нужен Python 3 новее системного 3.9. Тогда поставьте параллельный пакет python3.11 и дайте приложению собственный virtualenv.
Шифры и криптополитика
AlmaLinux 9 применяет общесистемную криптополитику. При DEFAULT выключены TLS 1.0 и 1.1, отвергаются подписи SHA-1 и ключи RSA короче 2048 бит. Это касается и исходящих соединений, поэтому скрипт, который ходит к старому эндпоинту партнёра, на новой машине может отвалиться, хотя на старой работал. Сначала посмотрите, потом ослабляйте ровно настолько, насколько вынуждены:
update-crypto-policies --show
update-crypto-policies --set DEFAULT:SHA1
После этого перезапустите затронутые сервисы: долгоживущие демоны читают политику при старте. Две смежные ловушки: OpenSSL 3 не откроет старые файлы PKCS#12 с RC2, нужен openssl pkcs12 -legacy -in old.pfx, а SSH-подключение по старому ключу ssh-rsa с SHA-1 будет отклонено. Правку важно положить в нужное место. В начале /etc/ssh/sshd_config стоит Include /etc/ssh/sshd_config.d/*.conf, штатный 50-redhat.conf подключает файл криптополитики, где PubkeyAcceptedAlgorithms уже задан, а sshd для каждого параметра берёт первое найденное значение. Строка, дописанная в конец sshd_config, молча игнорируется, и для того, кто уже не может зайти на сервер, хуже исхода не бывает. Кладите её в отдельный файл, который сортируется раньше:
echo 'PubkeyAcceptedAlgorithms +ssh-rsa' > /etc/ssh/sshd_config.d/40-legacy-rsa.conf
sshd -t && systemctl reload sshd
Всё это считайте временными мостиками с конкретной датой окончания.
От iptables к nftables и firewalld
Сохранённый набор правил iptables просто так не загрузится. Честные варианты: переписать политику на firewalld, что большинству серверов и стоит сделать, либо перевести набор, если в нём есть настоящая логика. Для перевода поставьте на новой машине совместимые утилиты, сконвертируйте и прочитайте результат до применения. Делайте это с открытым доступом к консоли: переведённый набор правил закроет ваш же SSH так же легко, как и написанный руками. И выберите одного хозяина файрвола: если грузите набор через nftables.service, отключите firewalld, иначе они будут перезатирать друг друга.
dnf install -y iptables-nft nftables
iptables-restore-translate -f /root/inventory-iptables.rules > /root/ruleset.nft
nft -c -f /root/ruleset.nft
nft -f /root/ruleset.nft
Для большинства задач firewalld проще и понятнее тому, кто примет сервер после вас:
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" port port="3306" protocol="tcp" accept'
firewall-cmd --reload
firewall-cmd --list-all
Два момента. В firewalld 1.x убрали дрейф зон и изменили умолчания по пересылке внутри зоны, поэтому конфигурация, скопированная из инструкции времён EL7, может вести себя иначе. И ipset вместе с iptables-nft в этом релизе объявлены устаревшими, так что новое на них не стройте. Если используете fail2ban, явно укажите действие через nftables или firewalld, а затем убедитесь командой nft list ruleset, что бан действительно ставится: молча неработающий fail2ban хуже, чем его отсутствие.
SELinux снова включён
Почти на каждой живой машине с CentOS 7 стоит SELINUX=disabled. Свежий AlmaLinux 9 работает в режиме enforcing, и симптомы выглядят как ошибки приложения: Apache отдаёт 403 на корне вне /var/www, php-fpm не может писать сессии, сервис не открывает нестандартный порт, исходящее соединение отвергается без видимой причины. Читайте отказы, а не гадайте:
dnf install -y policycoreutils-python-utils setroubleshoot-server
ausearch -m AVC -ts recent
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/app/storage(/.*)?"
restorecon -Rv /var/www/app
setsebool -P httpd_can_network_connect on
semanage port -m -t http_port_t -p tcp 8080
Этот блок задуман как меню, а не как скрипт. Выполните установку и ausearch, а дальше только то исправление, которое отвечает увиденному отказу. Особенно это касается setsebool -P httpd_can_network_connect on: он навсегда разрешает любому процессу httpd ходить в сеть наружу, поэтому не трогайте его, пока отказ этого не потребует. Обратите внимание на -m в строке про порт: с -a команда падает с ValueError: Port tcp/8080 already defined, потому что в targeted-политике 8080 уже имеет тип http_cache_port_t, и тип нужно менять, а не добавлять. Портам 8008, 8009, 8443 и 9000 команда не нужна вовсе, они уже относятся к http_port_t. Утилита semanage между релизами переехала в другой пакет: в EL7 это был policycoreutils-python, а в EL8 и EL9 он называется policycoreutils-python-utils. После большого rsync переразметьте скопированные данные командой restorecon -R или запустите полную переразметку через touch /.autorelabel и перезагрузку. Если застряли посреди миграции, setenforce 0 переводит SELinux в permissive, где можно собрать все отказы за один проход, исправить их и вернуть enforcing. Не выключайте его насовсем: в AlmaLinux 9 строка SELINUX=disabled в /etc/selinux/config больше не отключает его как раньше, ядро стартует с включённым SELinux и без загруженной политики, а полностью отключают его параметром ядра: grubby --update-kernel ALL --args selinux=0. Такое решение надо уметь обосновать.
Тестирование до того, как это увидят другие
Направьте свою рабочую станцию на новый IP записью в hosts и пользуйтесь приложением по-настоящему: залогиньтесь, загрузите файл, пройдите оформление заказа, вызовите отправку писем, запустите руками каждое задание cron от имени его владельца. И одновременно смотрите в логи:
journalctl -u php-fpm -u httpd -p warning --since "15 min ago"
tail -f /var/log/php-fpm/www-error.log
Две вещи забывают всегда. Исходящая почта завязана на новый IP: проверьте PTR-запись, SPF и DKIM до переключения, а не после первого отлупа. И каждому партнёру, который фильтрует по вашему IP, платёжному провайдеру, банку, API поставщика, новый адрес нужно добавить заранее, за несколько дней. На первые дни включите ещё и лог медленных запросов: оптимизатор MariaDB 10.11 не всегда выбирает тот же план, что MySQL 5.7, и запрос, который был быстрым, под реальной нагрузкой может просесть.
Само переключение
Сначала снизьте TTL записей DNS, и сделайте это заранее. Если у записи сейчас TTL в 24 часа, сегодняшнее изменение завтра не поможет: резолверы держат старое значение до истечения старого TTL. Опустите его до 300 секунд, выждите прежний TTL и только потом назначайте окно.
В день переезда: включите режим обслуживания, прогоните финальный rsync с rsync -aHAX --numeric-ids --delete, снимите финальный дамп базы, импортируйте, переразметьте, поднимите сервисы, проверьте новую машину через запись в hosts и только затем меняйте DNS. Старый сервер оставьте включённым и следите за access-логами обоих, пока старый не затихнет: обычно это занимает время TTL плюс хвост из клиентов с плохим кэшированием. Через день или два верните TTL обратно.
Старый сервер как окно отката
Не отказывайтесь от старой машины на той же неделе. Оставьте её на неделю или две, но остановите на ней приложение и переведите базу в режим только чтения, иначе клиент с закэшированным ответом DNS запишет заказ в базу, в которую больше никто никогда не заглянет. Именно это расхождение данных и есть реальный риск после переключения, а не сама миграция. Будьте честны насчёт окна отката: оно чистое в первые часы, а после того как на новом сервере появились настоящие записи, возврат означает ручное сведение данных.
Путь второй: обновление на месте через ELevate
Иногда пересборка невозможна: лицензия привязана к железу, машина выполняет роль устройства, или на ней десятилетний слой недокументированных локальных скриптов. ELevate от AlmaLinux, построенный на leapp, умеет конвертировать на месте. Два факта сразу. Прямого пути с CentOS 7 на AlmaLinux 9 нет. Сначала CentOS 7 на AlmaLinux 8, затем AlmaLinux 8 на AlmaLinux 9: два обновления, два окна с перезагрузкой и период на промежуточном релизе. И ELevate обновляет операционную систему, а не ваше приложение: вся описанная выше работа с PHP, базой и криптографией остаётся, только теперь на живом сервере.
Начните с переключения yum на vault, как показано выше. Здесь это не опция, а первый шаг: на нетронутой машине с CentOS 7 mirrorlist.centos.org отвечает 404, yum update останавливается с Cannot find a valid baseurl for repo: base/7/x86_64, а leapp блокирует обновление системы, обновлённой не полностью. Перед первой командой снимите снапшот или полную резервную копию и приготовьте доступ к консоли вне сети: уже первая строка обновляет машину, которую не обновляли годами, и затем её перезагружает. Когда репозиторий vault подключён:
yum update -y && reboot
yum install -y https://repo.almalinux.org/elevate/elevate-release-latest-el$(rpm --eval %rhel).noarch.rpm
yum install -y leapp-upgrade leapp-data-almalinux
leapp preupgrade
Прочитайте /var/log/leapp/leapp-report.txt целиком и снимите каждый блокирующий пункт. Типичные: сторонние репозитории без карты соответствия, изменённый sshd_config, который запрещает вход root на фазе обновления, и неподдерживаемые модули ядра. В конце отчёта есть ещё как минимум один вопрос, на который надо ответить явно, на CentOS 7 почти всегда про pam_pkcs11, и без ответа leapp upgrade просто не запустится. Отвечайте осознанно, а не копируйте строку: confirm=True говорит leapp, что вы согласны на удаление модуля pam_pkcs11: для обычного сервера так и есть, а для сервера со входом по смарт-картам нет. Когда отчёт чистый:
leapp answer --section remove_pam_pkcs11_module_check.confirm=True
leapp upgrade
reboot
Эта перезагрузка грузит специальный initramfs обновления и идёт долго. Именно здесь пригодится доступ к консоли: если загрузка встанет там, SSH уже не вернётся, а отменить всё можно только снимком, снятым заранее. Затем повторите последовательность на AlmaLinux 8, чтобы дойти до 9, начиная с установки elevate-release и уже через dnf, а не yum: переключение на vault относится к CentOS 7 и здесь не нужно. После этого разберите оставшееся:
rpm -qa | grep -E 'el7|el8'
find /etc -name '*.rpmnew' -o -name '*.rpmsave'
systemctl list-units --state=failed
Где ELevate действительно не подходит: серверы с панелями управления, у которых есть собственные средства миграции от вендора, машины с нестандартным хранилищем и всё, к чему нельзя получить консоль. Для машины в колокейшене, которую нельзя просто продублировать, этот путь разумен. Для публичного веб-сервера сборка нового почти всегда быстрее и безопаснее.
Следующая неделя
Проверьте, что резервные копии на новом сервере действительно создались и что восстановление работает, а не только что задание завершилось без ошибки. Убедитесь, что logrotate обрабатывает новые пути логов, что агент мониторинга отчитывается и что автоматические обновления безопасности настроены через dnf-automatic. Перезагрузки снова имеют значение: после обновления ядра или glibc она нужна, а dnf needs-restarting -r подскажет, когда. Если после переезда что-то ведёт себя странно, наши инженеры на связи круглосуточно, а ИТ-поддержка может сравнить новую машину со старой, пока та ещё жива.
Читайте дальше
Готовы начать?
Запустите за минуты или обсудите с инженером, что подходит вашему проекту.