Kubernetes tips & tricks: перевод работающих в кластере ресурсов под управление Helm 2

opjf_y39gfjotkthxdtaqn3cnhe.png

Необходимость подхвата ресурсов кластера Kubernetes может возникнуть в боевых условиях, когда нельзя просто пересоздать их инструментами Helm. Можно выделить две основные причины:

  • Будет простой — вне зависимости от того, облако у вас или bare metal.
  • При удалении могут потеряться сервисы в облаках, а также слетят связанные Load Balancer’ы в Kubernetes.


В нашем же случае, решение потребовалось для подхвата работающих ingress-nginx’ов при интеграции нашего оператора Kubernetes.

Для Helm категорически недопустимо, чтобы ресурсы, которыми он управляет, были созданы не им.

«Если в вашей команде ресурсы релиза могут изменяться вручную, готовьтесь столкнуться с проблемами, описанными в разделе: [BUG] После выката состояние ресурсов релиза в кластере не соответствуют описанному Helm-чарту». (из нашей прошлой статьи)


Как уже отмечалось ранее, Helm работает следующим образом:

  1. При каждой инсталляции (команды helm install, helm upgrade) Helm сохраняет сгенерированные манифесты релиза в storage backend. По умолчанию используется ConfigMaps: для каждой ревизии релиза создаётся ConfigMap в том же пространстве имён, в котором запущен Tiller.
  2. При повторных выкатах (команда 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, но со специальной заглушкой, которая позже заменяется на манифесты выбранных ресурсов.

Реализация


Алгоритм решения — следующий:

  1. Подготавливаем файл manifest.yaml с манифестами ресурсов для усыновления (подробнее этот пункт будет разобран ниже).
  2. Создаём чарт, в котором один единственный template со временным ConfigMap, т.к. Helm не сможет создать релиз без ресурсов.
  3. Создаём манифест templates/stub.yaml с заглушкой, что по длине будет равна количеству символов в manifest.yaml (в процессе экспериментов выяснилось, что количество байтов должно совпадать). В качестве заглушки должен выбираться воспроизводимый набор символов, который останется после генерации и сохранится в storage backend. Для простоты и наглядности используется #, т.е.:
    {{ repeat ${manifest_file_length} "#" }}
    
  4. Выполняем установку чарта: helm install и helm upgrade --install.
  5. Заменяем заглушку в 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}'"}}'
    
  6. Проверяем, что Tiller доступен и подхватил наши изменения.
  7. Удаляем временный ConfigMap (из второго шага).
  8. Далее работа с релизом ничем не отличается от штатной.


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


039LktlJS9CQsiy4ytlNluB7z.svg

Пример 2


iiG7oOKsghV4pAlOAF7iNgYFv.svg

Заключение


Описанное в статье решение может быть доработано и использоваться не только для усыновления Kubernetes-ресурсов с нуля, но и для добавления их в существующие релизы.

В настоящий момент нет решений, позволяющих подхватить существующие в кластере ресурсы, перевести их под управление Helm. Не исключено, что в Helm 3 будет реализовано решение, покрывающее данную проблему (по крайней мере, есть proposal на этот счёт).

P.S.


Другое из цикла K8s tips & tricks:
Читайте также в нашем блоге:

© Habrahabr.ru