Практические истории из наших SRE-будней. Часть 6

В очередном сборнике из недавних кейсов в нашей практике расскажу, как мы продлевали root-сертификаты Let’s Encrypt для старой CentOS, боролись с внезапным переключением DNS и Ingress, решали непростую задачу с шардами в Elasticsearch и не только.

bd3434ffced6a545e0ac03c5d614269c.png

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

История 1. Легко ли сегодня установить пакет в CentOS 6?

В 2020 году поддержка CentOS 6 прекратилась. Все знают, что надо своевременно обновлять ОС, но в реальности так не бывает. Вот и у нас оказалось некоторое количество серверов с CentOS 6.5.

Более того, потребовалось еще срочно установить на нее несколько пакетов. В обычной ситуации это сводится к выполнению yum install , но есть нюансы. 

Первая проблема на нашем пути: в файле /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 у нас хотя и работает, но не полностью: нет определенных алгоритмов шифрования — это и стало причиной проблемы.

Есть несколько вариантов решения:  

  1. Скачать пакеты на локальную машину по HTTPS, после чего загрузить по SCP/Rsync на удаленную. Это, возможно, самый правильный вариант.

  2. Взять файлы на одном из двух официальных зеркал, у которых пока нет редиректа с 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 </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 .

История 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: 
  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.

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

  1. Пользователь, под которым выполняется репликация, будет настроен на подключение с любого IP (например, 'replication'%'*').

  2. База будет не старее, чем хранящиеся 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«ы.

Как обычно устроен процесс:

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«ов у которых меньше нужного.

Продолжение следует

А вот что было в прошлых «сериях»:

© Habrahabr.ru