Kubernetes tips & tricks: перевод работающих в кластере ресурсов под управление Helm 2
Необходимость подхвата ресурсов кластера 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
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"}
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:
Читайте также в нашем блоге: