Привет, Хабр! Меня зовут Антон, я DevOps-инженер в YADRO, работаю с платформой машинного обучения. Недавно столкнулся с интересным случаем, над которым мне пришлось поломать голову. Одной из задач нашей команды стало развертывание helm-чарта для Airflow с использованием ArgoCD. Это потребовалось для реализации DAG-пайплайнов, необходимых для обработки данных и автоматизации процессов в проектах машинного обучения.

В статье расскажу о сложностях при расшифровке секретов с использованием плагина ArgoCD Vault, о паттерне App of Apps для обхода этих сложностей и небольшом погружении в детали установки плагина в кластер, из-за которых возможно неочевидное поведение ArgoCD Applications.


Итак, мне нужно было развернуть Helm-чарт для Airflow через ArgoCD.

Как известно, ArgoCD реализует концепцию GitOps и подразумевает хранение манифестов в репозитории. Но часть данных в values чувствительна, например пароль от базы данных PostgreSQL. Поэтому неплохо было бы вынести эти данные в хранилище секретов, чтобы скрыть информацию от лишних глаз. Для хранения секретов мы используем HashiCorp Vault.

Есть несколько способов подтянуть секреты из Vault в поды. Наиболее предпочтительный по ряду причин — vault-injector. Как минимум потому, что ArgoCD image-updater на данный момент не работает с плагинами, о которых пойдет речь в статье. В обычной ситуации я бы воспользовался vault-injector, но в случае с  helm-чартом Airflow задача показалась непростой. Поэтому я решил воспользоваться менее предпочтительным, но точно рабочим (как я думал на тот момент) вариантом с ArgoCD Vault Plugin.

Vault Plugin: что за плагин такой

Vault Plugin — это расширение функционала ArgoCD, которое позволяет использовать данные из различных хранилищ секретов, таких как HashiCorp Vault или AWS Secrets Manager, в сущностях Kubernetes.

Настроив этот плагин по инструкции, вы получите возможность внедрять секреты прямо в values, используя следующий синтаксис:

secret: <path:path/to/secret/in/vault#secret_key>

Это рабочий подход, но у него есть два нюанса, о которых полезно знать:

  • ArgoCD image-updater не будет работать или придется придумывать workaround. Это связано с известной особенностью ArgoCD — использовать несколько плагинов одновременно с ним не получится.

  • Если вам нужно будет обновить секреты в манифесте после того, как они обновились в хранилище, придется сделать Hard Refresh. Тот же vault-injector делает это без ручного вмешательства, и приложение может перечитать конфигурацию из присоединенного файла.

Где обнаружилась проблема

Когда секреты были добавлены в хранилище, а ArgoCD Application — написан, я попытался развернуть его для теста. Приведу примерный Application, с которым это делалось (весомая часть пропущена для компактности):

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
 name: airflow
 labels:
   app.kubernetes.io/name: airflow
   app.kubernetes.io/component: airflow
 namespace: argocd
 finalizers:
   - resources-finalizer.argocd.argoproj.io
spec:
 project: default
 destination:
   namespace: some-namespace
   name: cluster
 source:
   repoURL: "airflow_repo_url"
   targetRevision: "revision"
   chart: airflow
   plugin:
     name: argocd-vault-plugin-helm
     env:
       - name: HELM_VALUES
         value: |
             ...
             metadataConnection:
               user: user
               pass: <path:path/to/airflow/secrets#postgres_password>
               protocol: postgresql
               host: postgres.db.url
               port: 5432
               db: airflow_db
               sslmode: prefer
             ...
   
 syncPolicy:
   automated:
     prune: true
     selfHeal: true
   syncOptions:
     - Validate=true
     - CreateNamespace=true

Как видно, ничего необычного, за исключением прокидывания values прямо из Application и того самого секрета.

К моему удивлению, компонент webserver отказывался запускаться, ссылаясь на невозможность подключиться к базе данных. Хотя в правильности данных я был абсолютно уверен. После долгого дебага я наконец решил посмотреть, что же лежит в Kubernetes Secret, который создает чарт и откуда берется пароль. Вот, что я увидел:

apiVersion: v1
data:
 // base64 encoded
 postgres_password: %3Cpath%3Apath%2Fto%2Fairflow%2Fsecrets%23postgres_password%3E

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

После долгих попыток понять, в чем дело, я решил развернуть приложение иначе. Кроме прямого разворачивания Application, есть и другой вариант — App of Apps. Подробнее о нем можно почитать в официальной документации, но, если коротко, это способ создать базовое приложение (ArgoCD Application), которое будет подтягивать из директории в Git-репозитории другие приложения. И — о чудо! — все заработало.

В чем же была проблема

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

Это наблюдение натолкнуло меня на мысль, что первоначальный корень проблемы лежит где-то в самом чарте Airflow. И я не прогадал.

Вот что мы видим в манифесте секрета metadata-connection:

connection: {{ urlJoin (dict "scheme" .protocol "userinfo" (printf "%s:%s" (.user | urlquery) (.pass | urlquery) ) "host" (printf "%s:%s" $host $port) "path" (printf "/%s" $database) "query" $query) | b64enc | quote }}

Что такое urlquery, согласно официальной документации Helm:

Returns the escaped version of the value passed in as an argument so that it is suitable for embedding in the query portion of a URL.

Виновник экранированных данных в объекте Secret найден. Осталось только понять, почему строка сначала была проэкранирована, а не расшифрована.

Тут придется вспомнить, как именно argocd-vault-plugin устанавливается в кластер. Обратимся к официальной документации и найдем в ней ссылку на пример манифеста ConfigMap, который объясняет происходящее волшебство:

apiVersion: v1
kind: ConfigMap
metadata:
 name: argocd-cm
data:
 configManagementPlugins: |
   - name: argocd-vault-plugin
     generate:
       command: ["argocd-vault-plugin"]
       args: ["generate", "./"]


   - name: argocd-vault-plugin-helm
     generate:
       command: ["sh", "-c"]
       args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" . | argocd-vault-plugin generate -']


   # This lets you pass args to the Helm invocation as described here: https://argocd-vault-plugin.readthedocs.io/en/stable/usage/#with-helm
   # IMPORTANT: passing $helm_args effectively allows users to run arbitrary code in the Argo CD repo-server.
   # Only use this when the users are completely trusted. If possible, determine which Helm arguments are needed by
   # your users and explicitly pass only those arguments.
   - name: argocd-vault-plugin-helm-with-args
     generate:
       command: ["sh", "-c"]
       args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" ${helm_args} . | argocd-vault-plugin generate -']


   # This lets you pass a values file as a string as described here:
   # https://argocd-vault-plugin.readthedocs.io/en/stable/usage/#with-helm
   - name: argocd-vault-plugin-helm-with-values
     generate:
       command: ["bash", "-c"]
       args: ['helm template "$ARGOCD_APP_NAME" -n "$ARGOCD_APP_NAMESPACE" -f <(echo "$ARGOCD_ENV_HELM_VALUES") . | argocd-vault-plugin generate -']


   - name: argocd-vault-plugin-kustomize
     generate:
       command: ["sh", "-c"]
       args: ["kustomize build . | argocd-vault-plugin generate -"]

Главное, что нужно тут отметить: сперва вызывается helm template, который превращает шаблон в готовый манифест, в том числе выполняя urlquery. И только после этого подменяются секреты. Помните, что у указания секретов для плагина строгий синтаксис?

secret: <path:path/to/secret/in/vault#secret_key>

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

Заключение

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

Сталкивались ли вы с подобными проблемами на практике? И как их решали? Делитесь в комментариях. 

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