В очередном сборнике из недавних кейсов в нашей практике расскажу, как мы продлевали root-сертификаты Let's Encrypt для старой CentOS, боролись с внезапным переключением DNS и Ingress, решали непростую задачу с шардами в Elasticsearch и не только.
Важно. Предлагаемые в историях технические решения не претендуют на звание универсальных — перенимать их следует с осторожностью. Многое зависит от особенностей конкретного проекта, поэтому лучше изучить все возможные альтернативы.
История 1. Легко ли сегодня установить пакет в CentOS 6?
В 2020 году поддержка CentOS 6 прекратилась. Все знают, что надо своевременно обновлять ОС, но в реальности так не бывает. Вот и у нас оказалось некоторое количество серверов с CentOS 6.5.
Более того, потребовалось еще срочно установить на нее несколько пакетов. В обычной ситуации это сводится к выполнению yum install <foobar>
, но есть нюансы.
Первая проблема на нашем пути: в файле /etc/yum/yum.repos.d/CentOS-Base.repo
используется mirrorlist
(URL со списком зеркал). Он уже неактуальный (конечно, при условии, что там еще ничего не правили) и отключен. Как следствие, скачать что-то оттуда невозможно.
Окей, пробуем переключиться на основной репозиторий. Увы, это не помогает. Потому что, согласно плану поддержки версий CentOS, дистрибутивы старых версий ОС уезжают в хранилище на отдельный домен vault.centos.org и недоступны в основном репозитории.
Хорошо, отключаем Centos-Base.repo
(можно удалить или переместить этот файл из /etc/yum/yum.repos.d
, или указать enabled=0
для каждого репозитория в файле). А репозитории из файла Centos-Vault.repo
делаем активными: enabled=1
.
Пробуем yum install
— и снова неудача. Хотя для получения файлов из репозитория в конфигурации указан протокол HTTP, у vault.centos.org включен редирект с HTTP на HTTPS. В 2011 году, когда появился CentOS 6, все было не так, но теперь редирект на HTTPS — это скорее правило, чем исключение. Таковы требования безопасности. В результате наша старенькая CentOS теперь просто не может подключиться к серверу, закрытому новым сертификатом:
curl http://vault.centos.org -L
curl: (35) SSL connect error
Ошибка понятна. Дальнейшая диагностика показала, что все дело в NSS (Network Security Services) — наборе библиотек для разработки защищенных кросс-платформенных клиентских и серверных приложений. Приложения, построенные при помощи NSS, могут использовать и поддерживать SSLv3, TLS и другие стандарты безопасности.
Для фикса необходимо обновить NSS. Но даже тут возникает проблема: мы не можем скачать обновленные пакеты, так как они доступны только по HTTPS. А HTTPS у нас хотя и работает, но не полностью: нет определенных алгоритмов шифрования — это и стало причиной проблемы.
Есть несколько вариантов решения:
Скачать пакеты на локальную машину по HTTPS, после чего загрузить по SCP/Rsync на удаленную. Это, возможно, самый правильный вариант.
Взять файлы на одном из двух официальных зеркал, у которых пока нет редиректа с HTTP на HTTPS: linuxsoft.cern.ch или mirror.nsc.liu (у третьего зеркала, указанного на vault.centos.org как официальное — archive.kernel.org, — настроен редирект, поэтому оно не подходит).
Быстрый рецепт со списком обновляемых пакетов, учитывая их зависимости:
mkdir update && cd update
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nspr-4.19.0-1.el6.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-3.36.0-8.el6.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-softokn-freebl-3.14.3-23.3.el6_8.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-sysinit-3.36.0-8.el6.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-util-3.36.0-1.el6.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-softokn-3.14.3-23.3.el6_8.x86_64.rpm
curl -O http://mirror.nsc.liu.se/centos-store/6.10/os/x86_64/Packages/nss-tools-3.36.0-8.el6.x86_64.rpm
sudo rpm -Uvh *.rpm
Все готово! Скрестили пальцы, пробуем установить пакет.
Неудача: мы все еще видим ошибку с SSL. В этот момент нам уже кажется, что проще переустановить ОС. Но на самом деле это не так, поэтому мы продолжаем.
Последняя проблема связана с неактуальным корневым сертификатом, так как домен vault.centos.org закрыт сертификатом Let's Encrypt (LE)*. С 30 сентября 2021 года старые ОС больше не доверяют сертификатам, подписанным LE.
* На момент выхода статьи последний пункт про LE и патчинг ca-bundle уже неактуален, потому что vault.centos.org теперь закрыт сертификатом, подписанным Amazon Root CA 1. Но обновить NSS все же придется.
curl https://letsencrypt.org
curl: (60) Peer certificate cannot be authenticated with known CA certificates
More details here: http://curl.haxx.se/docs/sslcerts.html
Для CentOS 6 эта проблема исправляется так: а) обновляем список корневых сертификатов; б) меняем срок действия сертификата в локальном защищенном хранилище.
При этом модифицированный сертификат будет по-прежнему восприниматься OpenSSL как валидный. Способ работает, потому что в CentOS 6 используется OpenSSL v1.0.1e — он не проверяет сигнатуру сертификатов, которые находятся в локальном защищенном хранилище.
curl -O http://mirror.nsc.liu.se/centos-store/6.10/updates/x86_64/Packages/ca-certificates-2020.2.41-65.1.el6_10.noarch.rpm
sudo rpm -Uvh ca-certificates-2020.2.41-65.1.el6_10.noarch.rpm
sudo sed -i "s/xMDkzMDE0MDExNVow/0MDkzMDE4MTQwM1ow/g" /etc/ssl/certs/ca-bundle.crt
sudo update-ca-trust
Итак, мы восстановили доступ к официальным репозиториям CentOS для yum, и у нас работает HTTPS. Теперь, если нужно, можно добавить репозиторий с последней версией CentOS — 6.10.
cat <<EOF >/etc/yum.repos.d/CentOS-6.10-Vault.repo
[C6.10-base]
name=CentOS-6.10 - Base
baseurl=http://vault.centos.org/6.10/os/\$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6
enabled=1
[C6.10-updates]
name=CentOS-6.10 - Updates
baseurl=http://vault.centos.org/6.10/updates/\$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6
enabled=1
[C6.10-extras]
name=CentOS-6.10 - Extras
baseurl=http://vault.centos.org/6.10/extras/\$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6
enabled=1
EOF
После этих манипуляций можно спокойно выполнить yum install <foobar>
.
История 2. Внезапное переключение DNS и Ingress
Вечер четверга не предвещал проблем. Вдруг один из клиентов пишет: «Мы тут немного ошиблись в настройке DNS. Теперь в Kubernetes-кластер смотрит домен, которого там нет. Поменять быстро не получится (бюрократия, все дела), да и TTL у записи большой. Можем что-то придумать?»
В итоге на вечер четверга у нас есть:
DNS-запись
real-host
, которая смотрит в кластер;время обновления DNS — примерно 3 дня.
Решение было очевидным: а давайте поднимем nginx-proxy в кластере и будем проксировать запросы к нужному сервису. Хотя зачем нам nginx — у нас же есть nginx ingress. А для проксирования наружу можно использовать external service.
Так и сделали. В итоге в кластере добавили:
apiVersion: v1
kind: Service
metadata:
name: real-external
spec:
ports:
- port: 443
protocol: TCP
targetPort: 443
type: ClusterIP
---
apiVersion: v1
kind: Endpoints
metadata:
name: real-external
subsets:
- addresses:
- ip: <real-host-ip>
ports:
- port: 443
protocol: TCP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
name: real-external
spec:
rules:
- host: real-host
http:
paths:
- backend:
serviceName:real-external
servicePort: 443
path: /
tls:
- hosts:
- real-host
secretName: real-tls
Понадобится еще SSL-сертификат в real-tls
— его можно выписать cert-manager’ом. Способ вполне допустимый.
В целом такой подход достаточно распространенный. Он используется не только в случаях, когда кто-то ошибся, но и когда, например, нужно открыть доступ через Kubernetes к ресурсам вне кластера (зачем это нужно — уже другой вопрос).
История 3. Шарада с шардами
Однажды мы задумались о шардировании большого индекса Elasticsearch. Но на сколько шардов?
Согласно документации Elastic, есть два условия на новое количество шардов (number_of_shards
):
новое количество должно быть кратно текущему количеству шардов;
внутреннее количество подшардов, которое задается при создании индекса —
number_of_routing_shards
, — должно быть кратно новому количеству шардов. (В нашем примере используем значение по умолчанию.)
К сожалению, number_of_routing_shards
не отдается в /index/_settings
. И если не знаешь, как создавался индекс, кажется, что количество подшардов не определить.
Но существует и обходной путь: можно вызвать split api
с числом шардов, на которое number_of_routing_shards
точно не делится — например, большое простое число. Вместе с ошибкой вернется number_of_routing_shards
:
POST index/_split/target-index
{
"settings": {
"index.number_of_shards": это_число
}
}
{
"error": {
...
"reason": "the number of routing shards [421] must be a multiple of the target shards [15]"
}
],
...
}
… или нет? Внезапно индекс делится на 421 шард, которые расползаются по узлам, а краснеть за наши ошибки приходится кластеру. Что случилось?
Оказывается, если в нашем индексе изначально один шард, Elasticsearch считает, что его можно разделить на любое количество новых шардов. Откатить изменения можно через read-only на индекс и shrink API (в нашем случае это были не production-данные, поэтому индекс с 421 шардом просто удалили).
История 4. Что может быть проще, чем восстановить таблицу из бэкапа?
Среда, утро, разгар рабочей недели. Нам пишет клиент: «Парни, а у нас же есть бэкапы БД “Лемминги” (все названия выдуманы и все совпадения случайны — прим. автора), нам нужна таблица 1. Мы случайно грохнули оттуда все данные до 01.01.2022. Данные не самые важные: если получится достать их сегодня, будет круто».
Во «Фланте» мы делаем бэкапы с помощью Borg, в том числе и бэкапы БД. Например, PostgreSQL мы бэкапим в Borg, принимая stdout примерно так:
pg_basebackup --checkpoint=fast --format=tar --label=backup --wal-method=fetch --pgdata=-
Когда нужно извлечь какую-то часть данных из бэкапа (например, одну таблицу), мы обычно:
извлекаем нужный дамп с pgdata;
помещаем содержимое дампа во временный каталог;
запускаем Postgres в Docker-контейнере, прокидывая развернутый архив с pgdata в volume контейнера.
Делается это так:
borg extract --stdout /backup/REPONAME::ARCHIVE_NAME | tar -xC /backup/PG-DATA
docker run -d -it -e POSTGRES_HOST_AUTH_METHOD=trust -v /backup/PG-DATA:/var/lib/postgresql/data postgres:11
После чего подключаемся к Postgres и извлекаем нужные данные: создаем дамп части данных, части таблиц и так далее (откатить базу целиком требуется очень редко).
Что ж, всё ясно и понятно. Инженер восстанавливает бэкап, запускает Docker, идет заварить кофе, а дальше начинаются чудеса: подключается к БД, чтобы получить нужные данные и… не обнаруживает их в бэкапе! Но ведь они должны быть там, время удаления клиент точно определил!
Если вы думаете, что это очередная история про то, что надо проверять бэкапы и что мы повторили подвиг GitHub, то нет. Забегая вперед, скажу: с резервной копией все было хорошо, во всяком случае на момент ее распаковки. В чем же тогда дело?
Все просто: оказалось, что бэкап делался с реплики БД, поэтому внутри бэкапа лежал файл recovery.conf*
. В момент старта контейнер Postgres, запущенный в Docker’е, решил, что самое время «догоняться» с мастера**. Конечно этого не планировалось: ведь мы хотели получить состояние некой таблицы на момент бэкапа, нам совсем не нужно было получить актуальное состояние базы еще раз.
* С версии PostgreSQL 12 и выше вместо файла recovery.conf используется файл postgresql.auto.conf с параметрами соединения и файл standby.signal, отвечающий за переход в режим recovery. При наличии файла standby.signal в каталоге с данными во время запуска PostgreSQL ситуация будет аналогичная.
** Такой сценарий возможен, когда у бэкап-сервера есть доступ к мастер-базе: например, они в одной L2-сети и доступ в pg_hba.conf открыт для всей подсети.
Правильным решением в этом случае было удаление файла recovery.conf
(standby.signal
) или recovery.signal
) перед запуском Docker-контейнера. Это мы и сделали при второй попытке.
В итоге данные были восстановлены и ни одна запись не пострадала.
Также мы внесли изменения во внутреннюю инструкцию по развертыванию временного бэкапа и проинформировали коллег-инженеров. Возможно, это помогло кому-то сократить время простоя production на часы.
Вывод: нужно проверять конфигурацию даже при запуске приложений в Docker.
Примечание про MySQL
Похожее поведение можно получить на MySQL с использованием утилит от Percona: XtraBackup и innobackupex.
При создании дампа создается полная копия базы, включая настройки репликации. Если этот дамп восстановить, запущенная с ним база может подключиться к основному кластеру и начать репликацию — но только если совпадут два условия:
Пользователь, под которым выполняется репликация, будет настроен на подключение с любого IP (например,
'replication'%'*'
).База будет не старее, чем хранящиеся binlog’и.
История 5. Pod’ов нет, но вы держитесь
Однажды в пространстве имен stage-кластера Kubernetes перестали создаваться новые Pod’ы:
kubectl scale deploy productpage-v2 -replicas=2
Kubectl get po | grep productpage-v2
```только один старый Pod```
Обычные ситуации, когда Pod создан, но контейнеры в нем не запущены, таковы:
Pending
(нет подходящего узла);CrashLoopBackOff
(Pod ожидает запуска после серии падений);ContainerCreating
(контейнеры создаются).
Но в нашем случае не было даже объекта Pod’а!
Kubectl describe deploy productpage-v2
Normal ScalingReplicaSet 17s deployment-controller Scaled up replica set productpage-v2-65fff6fcc9 to 2
Kubectl describe rs productpage-v2-65fff6fcc9
Warning FailedCreate 16s (x13 over 36s) replicaset-controller Error creating: Internal error occurred: failed calling webhook "sidecar-injector.istio.io": Post "https://istiod-v1x10x1.d8-istio.svc:443/inject?timeout=30s": no service port 443 found for service "istiod-v1x10x1"
Ага: оказалось, нам мешает какой-то вебхук. Что это?
Документация говорит, что есть два вида admission webhooks: validating и mutating. Они позволяют вмешиваться в создание объектов: в случае validating — разрешать или запрещать их создание, в случае mutating — изменять их.
Работает это так: при создании объекта API-server отдает его описание по адресу, указанному в хуке, и получает ответ, что сделать с этим объектом: разрешить / запретить / изменить.
Вот типичный пример хука, который будет вызываться каждый раз при создании или обновлении любых объектов из cert-manager.io/v1:
webhooks:
- clientConfig:
...
service:
name: cert-manager-webhook <- здесь слушает cert-manager
namespace: d8-cert-manager
path: /mutate
port: 443
failurePolicy: Fail
...
rules:
- apiGroups:
- cert-manager.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- '*/*'
scope: '*'
В нашем случае сломался вебхук sidecar-injector.istio.io
. Его использует Istio, чтобы добавлять sidecar-контейнеры с Envoy во все запускаемые Pod’ы.
Как обычно устроен процесс:
Istio создает
MutatingWebhookConfiguration
.При создании Pod’а API-сервер идет в сервис
istiod-v1x10x1
:
service:
name: istiod-v1x10x1
namespace: d8-istio
path: /inject
port: 443
Istio отвечает, как именно он хочет изменить Pod, и Pod создается.
В этот раз Istio был сломан (точнее — удален, но не до конца: сервис istiod-v1x10x1
уже никуда не вел, а вебхук остался). При этом в вебхуке было указано failurePolicy: Fail
. Поэтому Pod и не создавался.
Если установить значение failurePolicy: Ignore
, можно создавать Pod принудительно, но, на наш взгляд, ошибку лучше мониторить, а не прятать. Обнаруживать ее помогает, например, модуль Deckhouse extended-monitoring. У него есть алерты на контроллеры, число Pod’ов у которых меньше нужного.
Продолжение следует
А вот что было в прошлых «сериях»:
«Практические истории из наших SRE-будней. Часть 5» (перенос Ceph между бэкендами; остановка бесконечного падения liveness- и readiness-проб; устранение бага в K8s-операторе для Redis);
«Практические истории из наших SRE-будней. Часть 4» (проблема ClickHouse с ZooKeeper, обновление MySQL, битва Cloudflare с Kubernetes);
«Практические истории из наших SRE-будней. Часть 3» (миграция Linux-сервера, Kubernetes-оператор ClickHouse, реплика PostgreSQL, обновление CockroachDB).