ArgoCD: деплой Helm-чарта и работа с Helm Secrets через AWS KMS

Автор: | 11/21/2020
 

В предыдущем посте ArgoCD: обзор, запуск, настройка SSL, деплой приложения потрогали ArgoCD, запустили тестовый инстанс, и задеплоили приложение из его готовых примеров.

Но наша цель — деплоить наши Helm-чарты, а потому посмотрим, как это можно сделать.

Самое интересное ожидаемо коснулось работы с Helm secrets. Пришлось покостылить, но в результате всё заработало так, как и ожидалось.

ArgCD: деплой Helm-чарта

Создаём тестовый чарт:

helm create test-helm-chart
Creating test-helm-chart

Проверяем его локально:

helm upgrade --install --namespace dev-1-test-helm-chart-ns --create-namespace test-helm-chart-release test-helm-chart/ --debug --dry-run
...
           {}
NOTES:
1. Get the application URL by running these commands:
 export POD_NAME=$(kubectl get pods --namespace dev-1-test-helm-chart-ns -l "app.kubernetes.io/name=test-helm-chart,app.kubernetes.io/instance=test-helm-chart-release" -o jsonpath="{.items[0].metadata.name}")
 export CONTAINER_PORT=$(kubectl get pod --namespace dev-1-test-helm-chart-ns $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
 echo "Visit http://127.0.0.1:8080 to use your application"
 kubectl --namespace dev-1-test-helm-chart-ns port-forward $POD_NAME 8080:$CONTAINER_PORT

Хорошо — чарт работает, пушим в репозиторий.

ArgoCD: подключение приватного репозитория

Github SSH-ключ

У нас Github-организация. Позже создадим отдельного пользователя для ArgoCD, у которого будет свой RSA-ключ доступа, пока добавим новый ключ своему Github-юзеру.

В общем-то можно сделать и через HTTPS и логин:токен, но через ключ уже привычнее.

Генерируем ключ:

ssh-keygen -f ~/.ssh/argocd-github-key
Generating public/private rsa key pair.
...

Добавляем его в Github — Settings > SSH keys:

ArgoCD repositories

Переходим в Settings — Reposistories:

Выбираем Connect repo using SSH:

Задаём имя, URL, приватный ключ:

Ключ будет сохранён в Kubernetes Secrets:

kk -n dev-1-devops-argocd-ns get secrets
NAME                                        TYPE                                  DATA   AGE
argocd-application-controller-token-mc457   kubernetes.io/service-account-token   3      45h
argocd-dex-server-token-74r75               kubernetes.io/service-account-token   3      45h
argocd-secret                               Opaque                                5      45h
argocd-server-token-54mfx                   kubernetes.io/service-account-token   3      45h
default-token-6mmr5                         kubernetes.io/service-account-token   3      45h
repo-332507798                              Opaque                                1      13m

repo-332507798 — вот он.

Жмём Connect.

Создание приложения

Создаём новое приложение:

Задаём имя, проект оставляем default, в Sync Policy можно включить опцию Auto-create namespace:

В Source оставляем Git, задаём URL репозитория, в Revision указываем бранч, в Path — путь к чарту.

В данном случае репозиторий devops-kubernetes, каталог с чартом — tests/test-helm-chart/, причём ArgoCD сам просканирует репозиторий, и предложит выбор каталогов:

В Destination выбираем локальный (в нашем случае) Kubernetes-кластер, указываем namespace, в который будем деплоить чарт:

В Destination, вместо Directory — указываем Helm, хотя Argo сам увидел, что это каталог с Helm-чартом, выбрал соответствующий тип и подгрузил значения из values.yaml в корне чарта, тут можно пока ничего не менять — позже в Values Files добавим наш secrets.yaml:

Готово:

Если кликнуть сейчас по приложению, то видим, что ArgoCD уже просканировал файлы шаблонов, и отобразил компоненты, которые будут задеплоены:

Кликаем Sync, и видим доступные опции — и Prune, с удалением, и Dry Run:

Кликаем Syncronize — пошёл деплой:

Всё задеплоилось и запустилось:

Проверим список приложений:

argocd app list
NAME             CLUSTER                         NAMESPACE                        PROJECT  STATUS  HEALTH   SYNCPOLICY  CONDITIONS  REPO                                                 PATH                   TARGET
guestbook        https://kubernetes.default.svc  default                          default  Synced  Healthy  <none>      <none>      https://github.com/argoproj/argocd-example-apps.git  guestbook              HEAD
test-helm-chart  https://kubernetes.default.svc  dev-1-devops-test-helm-chart-ns  default  Synced  Healthy  <none>      <none>      git@github.com:***/devops-kubernetes.git             tests/test-helm-chart  DVPS-458-ArgoCD

И под в неймспейсе:

kubectl -n dev-1-devops-test-helm-chart-ns get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-helm-chart-67dccc9fb4-2m5rf   1/1     Running   0          2m27s

А теперь приступим к работе с Helm secrets.

ArgoCD и Helm Secrets

Всё хорошо и просто, пока мы не дошли до секретов, т.к. Helm в ArgoCD не имеет предустановленных плагинов для Helm.

Из вариантов — собирать свой образ, рекомендовано министерством argoхранения тут>>>, либо — устанавливать в Kubernetes InitContainer через shared-volume, как описано тут>>>.

InitContainer с shared-volume

Первым вариантом я попробовал InitContainer с shared-volume, и в целом оно-то работает — плагин установился.

Выглядел Deployment для argocd-repo-server так:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: repo-server
    app.kubernetes.io/name: argocd-repo-server
    app.kubernetes.io/part-of: argocd
  name: argocd-repo-server
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: argocd-repo-server
  template:
    metadata:
      labels:
        app.kubernetes.io/name: argocd-repo-server
    spec:
      automountServiceAccountToken: false
      initContainers:
      - name: argo-tools
        image: alpine/helm
        command: [sh, -c]
        args:
        - apk add git &&
          apk add curl &&
          apk add bash &&
          helm plugin install https://github.com/futuresimple/helm-secrets
        volumeMounts:
        - mountPath: /root/.local/share/helm/plugins/
          name: argo-tools
      containers:
      - command:
        - uid_entrypoint.sh
        - argocd-repo-server
        - --redis
        - argocd-redis:6379
        image: argoproj/argocd:v1.7.9
        imagePullPolicy: Always
        name: argocd-repo-server
        ports:
        - containerPort: 8081
        - containerPort: 8084
        readinessProbe:
          initialDelaySeconds: 5
          periodSeconds: 10
          tcpSocket:
            port: 8081
        volumeMounts:
        - mountPath: /app/config/ssh
          name: ssh-known-hosts
        - mountPath: /app/config/tls
          name: tls-certs
        - mountPath: /app/config/gpg/source
          name: gpg-keys
        - mountPath: /app/config/gpg/keys
          name: gpg-keyring
        - mountPath: /home/argocd/.local/share/helm/plugins/
          name: argo-tools
      volumes:
      - configMap:
          name: argocd-ssh-known-hosts-cm
        name: ssh-known-hosts
      - configMap:
          name: argocd-tls-certs-cm
        name: tls-certs
      - configMap:
          name: argocd-gpg-keys-cm
        name: gpg-keys
      - emptyDir: {}
        name: gpg-keyring
      - emptyDir: {}
        name: argo-tools

Тут создаём emptyDir volume с именем argo-tools, запускаем initContainer с именем argo-tools, которому монтируем volume argo-tools в каталог /root/.local/share/helm/plugins/, устанавливаем git, curl и bash, и вызываем helm plugin install https://github.com/futuresimple/helm-secrets.

Этот же volume argo-tools монтируется к поду argocd-repo-server в каталог /home/argocd/.local/share/helm/plugins/ — и helm в контейнере argocd-repo-server плагин видит, и может использовать.

Но тут возникает проблема — как вызывать helm secrets install? Ведь ArgoCD по дефолту вызывает исполняемый файл /usr/local/bin/helm, и передать какие-то опции ему нельзя.

Поэтому пришлось всё-таки пилить костыль в виде кастомного образа, в который включаем helm-secrets, sops, и пишем wrapper-скрипт для вызова helm.

Сборка образа ArgoCD с плагином helm-secrets

Примеры решения нагуглились тут — How to Handle Kubernetes Secrets with ArgoCD and Sops.

Сначала — напишем наш wrapper-скрипт.

Задача скрипта — принимать вызовы к /usr/local/bin/helm с командами template, install, upgrade, lint и diff, которые понимает плагин helm-secrets, и передавать их в вызов helm secrets + все аргументы.

После выполнения helm secrets @arguments — выводится output выполнения helm secrets, из которого вырезается сообщение «removed ‘secrets.yaml.dec‘»:

#! /bin/sh
    
# helm secrets only supports a few helm commands
if [ $1 = "template" ] || [ $1 = "install" ] || [ $1 = "upgrade" ] || [ $1 = "lint" ] || [ $1 = "diff" ]
then 
    # Helm secrets add some useless outputs to every commands including template, namely
    # 'remove: <secret-path>.dec' for every decoded secrets.
    # As argocd use helm template output to compute the resources to apply, these outputs
    # will cause a parsing error from argocd, so we need to remove them.
    # We cannot use exec here as we need to pipe the output so we call helm in a subprocess and
    # handle the return code ourselves.
    out=$(helm.bin secrets $@) 
    code=$? 
    if [ $code -eq 0 ]; then
        # printf insted of echo here because we really don't want any backslash character processing
        printf '%s\n' "$out" | sed -E "/^removed '.+\.dec'$/d"      
        exit 0
    else
        exit $code
    fi
else
    # helm.bin is the original helm binary
    exec helm.bin $@
fi

Далее, надо собрать Docker-образ, в котором будут установлены helm-scerets, sops, и в котором вызов /usr/local/bin/helm будет заменён вызовом нашего скрипта.

Находим последнюю версию SOPS — https://github.com/mozilla/sops/releases/ и последнюю версию Helm-secrets — https://github.com/zendesk/helm-secrets/releases.

Пишем Dockerfile:

FROM argoproj/argocd:v1.7.9
    
ARG SOPS_VERSION="v3.6.1"
ARG HELM_SECRETS_VERSION="2.0.2" 

USER root  
COPY helm-wrapper.sh /usr/local/bin/
RUN apt-get update  --allow-insecure-repositories --allow-unauthenticated && \
    apt-get install -y \
    curl \
    gpg && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    curl -o /usr/local/bin/sops -L https://github.com/mozilla/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux && \
    chmod +x /usr/local/bin/sops && \
    cd /usr/local/bin && \
    mv helm helm.bin && \
    mv helm2 helm2.bin && \
    mv helm-wrapper.sh helm && \
    ln helm helm2 && \
    chmod +x helm helm2
    
# helm secrets plugin should be installed as user argocd or it won't be found
USER argocd
RUN /usr/local/bin/helm.bin plugin install https://github.com/zendesk/helm-secrets --version ${HELM_SECRETS_VERSION}
ENV HELM_PLUGINS="/home/argocd/.local/share/helm/plugins/"

Собираем образ — репозиторий в Docker Hub публичный, можно использовать этот образ.

Тегаем версией ArgoCD, которая использовалась, и свою версию сборки, тут это 1:

docker build -t setevoy/argocd-helm-secrets:v1.7.9-1 .
docker push setevoy/argocd-helm-secrets:v1.7.9-1

Далее, нам надо обновить install.yaml, из которого деплоился ArgoCD (Helm-чарт пока не использовал).

SOPS и  AWS KMS — аутентификация

«Какая боль, какая боль!» (с)

В нашем случае для шифрования данных используется AWS Key Management Service, следовательно SOPS в контейнере который мы запустим из образа setevoy/argocd-helm-secrets:v1.7.9-1 должен иметь к нему доступ.

Для SOPS требуются файлы ~/.aws/credentials и ~/.aws/config, который создадим из Kubernetes Secrets.

Наверно, можно было бы попробовать ServiceAccount с IAM ролью, которая давала бы доступ к ключу — но пока сделаю так.

AWS IAM User

Создадим отдельно пользователя для доступа к ключу — переходим в AWS IAM, задаём ему Programmatic access:

Далее, создаём политику на ReadOnly только к ключу, который используется SOPS:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "kms:ListKeys",
                "kms:ListAliases",
                "kms:DescribeKey",
                "kms:ListKeyPolicies",
                "kms:GetKeyPolicy",
                "kms:GetKeyRotationStatus",
                "iam:ListUsers",
                "iam:ListRoles"
            ],
            "Resource": "arn:aws:kms:us-east-2:534***385:key/f73daf0d-***-440ca3b6547b",
            "Effect": "Allow"
        }
    ]
}

Сохраняем, и подключаем её:

Сохраняем пользователя, переходим в AWS KMS, и добавляем Key User:

Настраиваем новый локальный AWS-профиль:

aws configure --profile argocd-kms
AWS Access Key ID [None]: AKI***Q4F
AWS Secret Access Key [None]: S7c***6ya
Default region name [None]: us-east-2
Default output format [None]:

Проверяем доступ к ключу:

aws --profile argocd-kms kms describe-key --key-id f73daf0d-***-440ca3b6547b
{
"KeyMetadata": {
"AWSAccountId": "534***385",
"KeyId": "f73daf0d-***-440ca3b6547b",
"Arn": "arn:aws:kms:us-east-2:534***385:key/f73daf0d-***-440ca3b6547b",
...

Этот профиль будем использовать при шифровании секрета, и его же надо добавить в под argocd-repo-server.

AWS credentials и config

Cоздаём новый секрет, в котором описываем файлы ~/.aws/credentials и ~/.aws/config, которые потом замапим в под argocd-repo-server:

---     
apiVersion: v1
kind: Secret
metadata:
  name: argocd-aws-credentials
  namespace: dev-1-devops-argocd-ns
type: Opaque
stringData: 
  credentials: |
    [argocd-kms]
    aws_access_key_id = AKI***Q4F
    aws_secret_access_key = S7c***6ya
  config: | 
    [profile argocd-kms]
    region = us-east-2

Добавляем его в .gitignore:

cat .gitignore
argocd-aws-credentials.yaml

В будущем, когда будет делаться автоматизация для развёртывания ArgoCD, можно будет его создавать из Jenkins Secrets.

Создаём секрет:

kubectl apply -f argocd-aws-credentials.yaml
secret/argocd-aws-credentials created

Обновляем Deployment argocd-repo-server — меняем используемый образ, добавляем новый volume из нашего секрета, и монтируем его каталогом /home/argocd/.aws в контейнере с Argo:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/component: repo-server
    app.kubernetes.io/name: argocd-repo-server
    app.kubernetes.io/part-of: argocd
  name: argocd-repo-server
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: argocd-repo-server
  template:
    metadata:
      labels:
        app.kubernetes.io/name: argocd-repo-server
    spec:
      automountServiceAccountToken: false
      containers:
      - command:
        - uid_entrypoint.sh
        - argocd-repo-server
        - --redis
        - argocd-redis:6379
#        image: argoproj/argocd:v1.7.9
        image: setevoy/argocd-helm-secrets:v1.7.9-1
        imagePullPolicy: Always
        name: argocd-repo-server
        ports:
        - containerPort: 8081
        - containerPort: 8084
        readinessProbe:
          initialDelaySeconds: 5
          periodSeconds: 10
          tcpSocket:
            port: 8081
        volumeMounts:
        - mountPath: /app/config/ssh
          name: ssh-known-hosts
        - mountPath: /app/config/tls
          name: tls-certs
        - mountPath: /app/config/gpg/source
          name: gpg-keys
        - mountPath: /app/config/gpg/keys
          name: gpg-keyring
        - mountPath: /home/argocd/.aws
          name: argocd-aws-credentials
      volumes:
      - configMap:
          name: argocd-ssh-known-hosts-cm
        name: ssh-known-hosts
      - configMap:
          name: argocd-tls-certs-cm
        name: tls-certs
      - configMap:
          name: argocd-gpg-keys-cm
        name: gpg-keys
      - emptyDir: {}
        name: gpg-keyring
      - name: argocd-aws-credentials
        secret:
          secretName: argocd-aws-credentials

Обновляем ArgoCD:

kubectl -n dev-1-devops-argocd-ns apply -f install.yaml

Проверяем поды:

kubectl -n dev-1-devops-argocd-ns get pod
NAME                                             READY   STATUS        RESTARTS   AGE
...
argocd-repo-server-64f4bbf4b7-jcs6x              1/1     Terminating   0          19h
argocd-repo-server-7c64775679-9jjq2              1/1     Running       0          12s

Проверяем файлы:

kubectl -n dev-1-devops-argocd-ns exec -ti argocd-repo-server-7c64775679-9jjq2 -- cat /home/argocd/.aws/credentials
[argocd-kms]
aws_access_key_id = AKI***Q4F
aws_secret_access_key = S7c***6ya

И пробуем использовать helm-secrets.

Добавление secrets.yaml

В репозитории с чартом создаём файл secrets.yaml:

somePassword: secretValue

Создаём .sops.yaml с указанием KMS-ключа для шифрования и AWS-профиля:

---
creation_rules:
  - kms: 'arn:aws:kms:us-east-2:534****385:key/f73daf0d-***-440ca3b6547b'
    aws_profile: argocd-kms

Шифруем файл:

helm secrets enc secrets.yaml
Encrypting secrets.yaml
Encrypted secrets.yaml

В тестовый чарт добавим использование секрета, например создание переменной TEST_SECRET_PASSWORD — обновляем файл templates/deployment.yaml:

...
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          env:
          - name: TEST_SECRET_PASSWORD
            value: {{ .Values.somePassword }}
...

Пушим изменения в репозиторий:

git add secrets.yaml templates/deployment.yaml
git commit -m "test secret added" && git push

Переходим в настройки приложения — App Details > Parameters, жмём Edit и добавляем values.yaml и secrets.yaml как Values Files:

ArgoCD теперь видит, что приложение рассинхронизировано с тем, что в репозитории:

Синхронизируем:

Проверяем новый под:

И напрямую в поде:

kubectl -n dev-1-devops-test-helm-chart-ns exec -ti test-helm-chart-5c777f9c9d-wkx6s -- printenv | grep SECRET
TEST_SECRET_PASSWORD=secretValue

Готово —  секрет на месте.