company_banner

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



    Необходимость подхвата ресурсов кластера 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




    Пример 2




    Заключение


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

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

    P.S.


    Другое из цикла K8s tips & tricks:


    Читайте также в нашем блоге:

    Флант
    352,00
    Специалисты по DevOps и Kubernetes
    Поделиться публикацией

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое