
Необходимость подхвата ресурсов кластера Kubernetes может возникнуть в боевых условиях, когда нельзя просто пересоздать их инструментами Helm. Можно выделить две основные причины:
- Будет простой — вне зависимости от того, облако у вас или bare metal.
- При удалении могут потеряться сервисы в облаках, а также слетят связанные Load Balancer'ы в Kubernetes.
В нашем же случае, решение потребовалось для подхвата работающих ingress-nginx'ов при интеграции нашего оператора Kubernetes.
Для Helm категорически недопустимо, чтобы ресурсы, которыми он управляет, были созданы не им.
«Если в вашей команде ресурсы релиза могут изменяться вручную, готовьтесь столкнуться с проблемами, описанными в разделе: [BUG] После выката состояние ресурсов релиза в кластере не соответствуют описанному Helm-чарту». (из нашей прошлой статьи)
Как уже отмечалось ранее, Helm работает следующим образом:
- При каждой инсталляции (команды
helm install
,helm upgrade
) Helm сохраняет сгенерированные манифесты релиза в storage backend. По умолчанию используется ConfigMaps: для каждой ревизии релиза создаётся ConfigMap в том же пространстве имён, в котором запущен Tiller. - При повторных выкатах (команда
helm upgrade
) Helm сравнивает новые сгенерированные манифесты со старыми манифестами последней DEPLOYED-ревизии релиза из ConfigMap, а получившуюся разницу применяет в Kubernetes.
Основываясь на этих особенностях, мы пришли к тому, что достаточно пропатчить ConfigMap (storage backend релиза), чтобы подхватить, т.е. усыновить существующие ресурсы в кластере.
Tiller именует ConfigMap в следующем формате:
%RELEASE_NAME.v%REVISION
. Чтобы получить существующие записи, необходимо выполнить команду kubectl get cm -l OWNER=TILLER --namespace kube-system
(по умолчанию Tiller устанавливается в пространство имён kube-system
— иначе необходимо указать используемый).$ kubectl get cm -l OWNER=TILLER -n kube-system
NAME DATA AGE
release_name_1.v618 1 5d
release_name_1.v619 1 1d
release_name_2.v1 1 2d
release_name_2.v2 1 3d
Каждый ConfigMap представлен в таком формате:
apiVersion: v1
data:
release: H4sIAHEEd1wCA5WQwWrDMAyG734Kwc52mtvwtafdAh27FsURjaljG1kp5O3nNGGjhcJ21M/nT7+stVZvcEozO7LAFAgLnSNOdG4boSkHFCpNIb55R2bBKSjM/ou4+BQt3Fp19XGwcNoINZHggIJWAayaH6leJ/24oTIBewplpQEwZ3Ode+JIdanxqXkw/D4CGClMpoyNG5HlmdAH05rDC6WPRTC6p2Iv4AkjXmjQ/WLh04dArEomt9aVJVfHMcxFiD+6muTEsl+i74OF961FpZEvJN09HEXyHmdOklwK1X7s9my7eYdK7egk8b8/6M+HfwNgE0MSAgIAAA==
kind: ConfigMap
metadata:
creationTimestamp: 2019-02-08T11:12:38Z
labels:
MODIFIED_AT: "1550488348"
NAME: release_name_1
OWNER: TILLER
STATUS: DEPLOYED
VERSION: "618"
name: release_name_1.v618
namespace: kube-system
resourceVersion: "298818981"
selfLink: /api/v1/namespaces/kube-system/configmaps/release_name_1.v618
uid: 71c3e6f3-2b92-11e9-9b3c-525400a97005
Сгенерированные манифесты хранятся в бинарном виде (в примере выше по ключу
.data.release
), поэтому создавать релиз мы решили штатными средствами Helm, но со специальной заглушкой, которая позже заменяется на манифесты выбранных ресурсов.Реализация
Алгоритм решения — следующий:
- Подготавливаем файл
manifest.yaml
с манифестами ресурсов для усыновления (подробнее этот пункт будет разобран ниже). - Создаём чарт, в котором один единственный template со временным ConfigMap, т.к. Helm не сможет создать релиз без ресурсов.
- Создаём манифест
templates/stub.yaml
с заглушкой, что по длине будет равна количеству символов вmanifest.yaml
(в процессе экспериментов выяснилось, что количество байтов должно совпадать). В качестве заглушки должен выбираться воспроизводимый набор символов, который останется после генерации и сохранится в storage backend. Для простоты и наглядности используется#
, т.е.:
{{ repeat ${manifest_file_length} "#" }}
- Выполняем установку чарта:
helm install
иhelm upgrade --install
. - Заменяем заглушку в storage backend релиза на манифесты ресурсов из
manifest.yaml
, которые были выбраны для усыновления на первом шаге:
stub=$(printf '#%.0s' $(seq 1 ${manifest_file_length})) release_data=$(kubectl get -n ${tiller_namespace} cm/${release_name}.v1 -o json | jq .data.release -r) updated_release_data=$(echo ${release_data} | base64 -d | zcat | sed "s/${stub}/$(sed -z 's/\n/\\n/g' ${manifest_file_path} | sed -z 's/\//\\\//g')/" | gzip -9 | base64 -w0) kubectl patch -n ${tiller_namespace} cm/${release_name}.v1 -p '{"data":{"release":"'${updated_release_data}'"}}'
- Проверяем, что Tiller доступен и подхватил наши изменения.
- Удаляем временный ConfigMap (из второго шага).
- Далее работа с релизом ничем не отличается от штатной.
Gist с описанной выше реализацией доступен по ссылке:
$ ./script.sh
Example:
./script.sh foo bar-prod manifest.yaml
Usage:
./script.sh CHART_NAME RELEASE_NAME MANIFEST_FILE_TO_ADOPT [TILLER_NAMESPACE]
В результате выполнения скрипта создаётся релиз
RELEASE_NAME
. Он связывается с ресурсами, манифесты которых описаны в файле MANIFEST_FILE_TO_ADOPT
. Также генерируется чарт CHART_NAME
, который может быть использован для дальнейшего сопровождения манифестов и релиза в частности.При подготовке манифеста с ресурсами необходимо удалить служебные поля, которые используются Kubernetes (это динамические служебные данные, поэтому неправильно версионировать их в Helm). В идеальном мире подготовка сводится к одной команде:
kubectl get RESOURCE -o yaml --export
. Ведь документация гласит: --export=false: If true, use 'export' for the resources. Exported resources are stripped of cluster-specific information.
… но, как показала практика, опция
--export
ещё сыровата, поэтому потребуется дополнительное форматирование манифестов. В манифесте service/release-name-habr
, представленном ниже, необходимо удалить поля creationTimestamp
и selfLink
.kubectl version
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.3", GitCommit:"721bfa751924da8d1680787490c54b9179b1fed0", GitTreeState:"clean", BuildDate:"2019-02-01T20:08:12Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"13", GitVersion:"v1.13.3", GitCommit:"721bfa751924da8d1680787490c54b9179b1fed0", GitTreeState:"clean", BuildDate:"2019-02-01T20:00:57Z", GoVersion:"go1.11.5", Compiler:"gc", Platform:"linux/amd64"}
kubectl get service/release-name-habr -o yaml --export
apiVersion: v1
kind: Service
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"labels":{"app.kubernetes.io/instance":"release-name","app.kubernetes.io/managed-by":"Tiller","app.kubernetes.io/name":"habr","helm.sh/chart":"habr-0.1.0"},"name":"release-name-habr","namespace":"default"},"spec":{"ports":[{"name":"http","port":80,"protocol":"TCP","targetPort":"http"}],"selector":{"app.kubernetes.io/instance":"release-name","app.kubernetes.io/name":"habr"},"type":"ClusterIP"}}
creationTimestamp: null
labels:
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Tiller
app.kubernetes.io/name: habr
helm.sh/chart: habr-0.1.0
name: release-name-habr
selfLink: /api/v1/namespaces/default/services/release-name-habr
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app.kubernetes.io/instance: release-name
app.kubernetes.io/name: habr
sessionAffinity: None
type: ClusterIP
status:
loadBalancer: {}
Далее представлены примеры использования скрипта. Оба они демонстрируют, как с помощью скрипта можно усыновить работающие в кластере ресурсы и впоследствии удалить их средствами Helm.
Пример 1
Пример 2
Заключение
Описанное в статье решение может быть доработано и использоваться не только для усыновления Kubernetes-ресурсов с нуля, но и для добавления их в существующие релизы.
В настоящий момент нет решений, позволяющих подхватить существующие в кластере ресурсы, перевести их под управление Helm. Не исключено, что в Helm 3 будет реализовано решение, покрывающее данную проблему (по крайней мере, есть proposal на этот счёт).
P.S.
Другое из цикла K8s tips & tricks:
- «Персонализированные страницы ошибок в NGINX Ingress»;
- «О выделении узлов и о нагрузках на веб-приложение»;
- «Доступ к dev-площадкам»;
- «Ускоряем bootstrap больших баз данных».
Читайте также в нашем блоге: