ArgoCD: декларативные Projects, Applications и деплой ArgoCD из Jenkins

Автор: | 05/18/2021
 

Создать новое приложение, кластер или репозиторий в ArgoCD можно как используя WebUI, так и описав его в виде Kubernetes-манифеста, который потом можно передать kubectl для создания ресурса.

Например, приложения являются CustomResources и описаны в Kubernets CRD applications.argoproj.io:

kubectl get crd applications.argoproj.io
NAME                       CREATED AT
applications.argoproj.io   2020-11-27T15:55:29Z

Которые потом доступны в неймспейсе ArgoCD в виде обычных Kubernetes-ресурсов:

kubectl -n dev-1-18-devops-argocd-ns get applications
NAME                              SYNC STATUS   HEALTH STATUS
backend-app                       OutOfSync     Missing
dev-1-18-web-payment-service-ns   Synced        Healthy
web-fe-github-actions             Synced        Healthy

Удобен этот подход тем, что при создании нового инстанса ArgoCD не надо будет создавать все приложения вручную. Например, мы при обновлении версии Kubernetes создаём новый кластер, в котором в Jenkins с помощью Ansible и Helm устанавливаются все контроллеры, в т.ч. ArgoCD. В эту же автоматизацию можно добавить и создание всех наших Projects и Applications, которые будут описываться девелоперами в репозитории.

Итак, наша задача настроить автоматическое создание:

  • Projects с ролями доступов и разрешениями на неймспейсы
  • Applications – для Бекенд и Веб проектов
  • Repositories – настроить аутентификацию в Github

Сначала посмотрим, как создавать Porjects и Applications из “голых” манифестов, а потом прикрутим это всё в Ansible и создадим Jenkins-джобу.

Projects

Документация – Projects.

Начнём с создания проекта. У нас их будет два, Backend и Web, для каждого надо будет ограничить используемые Namespace и создать роль с доступом к приложениям в этом проекте.

Создадим проект для бекенд-команды:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: backend
  namespace: dev-1-18-devops-argocd-ns
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  description: "Backend project"
  sourceRepos:
  - '*'
  destinations:
  - namespace: 'dev-1-18-backend-*'
    server: https://kubernetes.default.svc
  clusterResourceWhitelist:
  - group: ''
    kind: Namespace
  namespaceResourceWhitelist:
  - group: "*"
    kind: "*"
  roles:
  - name: "backend-app-admin"
    description: "Backend team's deployment role"
    policies:
    - p, proj:backend:backend-app-admin, applications, *, backend/*, allow
    groups:
    - "Backend"
  orphanedResources:
    warn: true

Тут:

  • sourceRepos: разрешаем деплоить в этом проекте из любых репозиториев
  • destinations: в какой кластер и неймспейсы можно будет деплоить приложения в этом проекте, в примере выше используем маску dev-1-18-backend-*, что бы разрешить деплой Бекенд-команды в любые неймспейсы, начинающиеся с dev-1-18-backend-*, а для Веб-команды соответственно будет dev-1-18-web-*
  • clusterResourceWhitelist: на уровне кластера разрешаем создание только Namespaces
  • namespaceResourceWhitelist: на уровне Namespace разрешаем создание любых ресурсов
  • roles: добавляем роль backend-app-admin с полными правами на приложения в этом проекте, см. ArgoCD: пользователи, доступы и RBAC и ArgoCD: интеграция с Okta и группы пользователей
  • orphanedResources: включаем вывод предупреждений о “заброшенных” ресурсах, см. Orphaned Resources Monitoring

Деплоим:

kubectl apply -f project.yaml
appproject.argoproj.io/backend created

Проверяем:

argocd proj get backend
Name:                             backend
Description:                      Backend project
Destinations:                     <none>
Repositories:                     *
Whitelisted Cluster Resources:    /Namespace
Blacklisted Namespaced Resources: <none>
Signature keys:                   <none>
Orphaned Resources:               enabled (warn=true)

Applications

Документация – Applications.

Далее, добавим описание тестового приложения, которое будет создаваться в проекте Backend:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-app
  namespace: dev-1-18-devops-argocd-ns
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: "backend"
  source:
    repoURL: https://github.com/projectname/devops-kubernetes.git
    targetRevision: DVPS-967-ArgoCD-declarative-bootstrap
    path: tests/test-svc
    helm:
      parameters:
      - name: serviceType
        value: LoadBalancer
  destination:
    server: https://kubernetes.default.svc
    namespace: dev-1-18-backend-argocdapp-test-ns
  syncPolicy:
    automated:
      prune: false
      selfHeal: false
      allowEmpty: false
    syncOptions:
    - Validate=true
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Тут:

  • finalizers: указываем на удаление всех ресурсов в Kubernetes при удалении приложения из ArgoCD (cascade deletion, см. App Deletion)
  • project: проект, в котором будет создано приложение и которые описывает применяемые к приложению ограничения (неймспейсы, ресурсы и т.д.)
  • source: задаём репозиторий и бранч
    • в helm.parameters – передаём значения для Helm-чарта (helm --set)
  • destination: кластер и неймспейс для деплоя приложения. При этом неймспейс должен быть задан в разрешённых в Project, которому это приложение принадлежит
  • syncPolicy: тип синхронизации, см. Automated Sync Policy и Sync Options

Кроме того, ArgoCD поддерживает так называемые App of Apps, когда одно приложение создаёт другое, а то в свою очередь следующее и так далее. Мы использовать (пока) не будем, но механизм интересный, см. Cluster Bootstrapping.

Деплоим наше приложение:

kubectl apply -f application.yaml
application.argoproj.io/backend-app created

Проверяем:

kubectl -n dev-1-18-devops-argocd-ns get application backend-app
NAME          SYNC STATUS   HEALTH STATUS
backend-app   Unknown       Healthy

Проверяем статус:

argocd app get backend-app
Name:               backend-app
Project:            backend
Server:             https://kubernetes.default.svc
Namespace:          dev-1-18-backend-argocdapp-test-ns
URL:                https://dev-1-18.argocd.example.com/applications/backend-app
Repo:               https://github.com/projectname/devops-kubernetes.git
Target:             DVPS-967-ArgoCD-declarative-bootstrap
Path:               tests/test-svc
SyncWindow:         Sync Allowed
Sync Policy:        Automated
Sync Status:        Unknown
Health Status:      Healthy
CONDITION                MESSAGE                                                   LAST TRANSITION
ComparisonError          rpc error: code = Unknown desc = authentication required  2021-05-18 09:11:13 +0300 EEST
OrphanedResourceWarning  Application has 1 orphaned resources                      2021-05-18 09:11:13 +0300 EEST

В ошибках у нас сейчас “ComparisonError = authentication required” – ArgoCD не смог подключиться к репозиторию, так как он приватный.

Переходим к настройкам репозиториев и аутентификации.

Repositories

Документация – Repositories.

Репозитории, внезапно, добавляются в argocd-cm ConfigMap, а не в виде отдельных ресурсов, как Applications или Projects.

Как по мне – не слишком удобное решение, так как если девелопер захочет добавить новый репозиторий – то ему придётся давать права на редактирование системного ConfigMap.

Кроме того, для аутентификации на Git-сервере используются секреты, которые хранятся в неймспейсе ArgoCD, значит – нужен будет доступ и туда.

Однако, у ArgoCD есть механизм, позволяющий выполнять аутентификацию на сервере используя единый секрет, см. Repository Credentials.

Тут идея заключается в том, что мы можем задать “маску”, по которой будет проверяться репозиторий, например, можно добавить репозиторий https://github.com/projectname, к нему подключить данные доступа. Затем, девелопер при создании Application указывает свой репозиторий в виде https://github.com/orgname/reponame, и тогда ArgoCD используя маску github.com/projectname выполнит аутентификацию в Github для доступа к репозиторию github.com/projectname/reponame.

Таким образом, мы можем задать единую “точку аутентификации”, и все девелоперы в их репозиториях будут использовать её, так как все репозитории проекта принадлежат организации orgname.

Repository Secret

Используем HTTPS и Github Access token.

Идём в Github профиль, создаём новый токен (правильнее будет завести отдельного пользователя для ArgoCD, и токен выдать ему):

Даём права на репозитории:

 

Енкодим токен в base64 в терминале или используя https://www.base64encode.org:

echo -n ghp_GE***Vx911 | base64
Z2h***xMQo=

И имя пользователя:

echo -n username | base64
c2V***eTI=

Описываем Kubernetes Secret, создаём его в неймспейсе ArgoCD:

apiVersion: v1
kind: Secret
metadata:
  name: github-access
  namespace: dev-1-18-devops-argocd-ns
data:
  username: c2V***eTI=
  password: Z2h***xMQ==

Применяем:

kubectl apply -f secret.yaml
secret/github-access created

Добавляем его использование в argocd-cm ConfigMap:

...
  repository.credentials: |
    - url: https://github.com/orgname
      passwordSecret:
        name: github-access
        key: password
      usernameSecret:
        name: github-access
        key: username
...

Проверяем:

argocd repocreds list
URL PATTERN                      USERNAME  SSH_CREDS  TLS_CREDS
https://github.com/projectname  username  false      false

И пробуем синхронизировать приложение:

argocd app sync backend-app
...
Message:            successfully synced (all tasks run)
GROUP  KIND     NAMESPACE                           NAME      STATUS  HEALTH       HOOK  MESSAGE
Service  dev-1-18-backend-argocdapp-test-ns  test-svc  Synced  Progressing        service/test-svc created

Jenkins, Ansible и ArgoCD

Теперь можно подумать об автоматизации всего этого дела.

У нас ArgoCD устанавливается из Helm-чарта в Ansible-роли с помощью community.kubernetes, по аналогии с Ansible: модуль community.kubernetes и установка Helm-чарта с ExternalDNS.

Что нам надо добавить:

  • в Ansible-роль добавим создание секрета для Github
  • в настройках ArgoCD сервера добавим создание repository.credentials
  • отдельно создадим каталоги с Projects и Applications, а в Ansible-роли добавим вызов community.kubernetes.k8s, которому будем скармливать манифесты приложений из этого каталога. Таким образом проекты и приложения будут создаваться автоматически при развёртывании кластера и установке нового инстанса ArgoCD, плюс девелоперы смогут сами добавлять манифесты с  их Applications

Ansible Kubernetes Secret

В файл переменных, в нашем случае общий group_vars/all.yaml, добавляем две переменные, которые шифруем с ansible-vault. Не забываем, что перед тем, как шифровать – строки надо перевести в base64.

Добавляем:

...
argocd_github_access_username: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          63623436326661333236383064636431333532303436323735363063333264306535313934373464
          ...
          3132663634633764360a666162616233663034366536333765666364643363336130323137613333
          3636
argocd_github_access_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          61393931663234653839326232383435653562333435353435333962363361643634626664643062
          ...
          6239623265306462343031653834353562613264336230613466
...

В файл роли, в данном случае roles/argocd/tasks/main.yml, добавляем создание секрета:

...
- name: "Create github-access Secret"
  community.kubernetes.k8s:
    definition:
      kind: Secret
      apiVersion: v1
      metadata:
        name: "github-access"
        namespace: "{{ eks_env }}-devops-argocd-2-0-ns"
      data:
        username: "{{ argocd_github_access_username }}"
        password : "{{ argocd_github_access_password }}"
...

В values чарта описываем создание repository.credentials, см. values.yaml:

...
- name: "Deploy ArgoCD chart to the {{ eks_env }}-devops-argocd-2-0-ns namespace"
  community.kubernetes.helm:
    kubeconfig: "{{ kube_config_path }}"
    name: "argocd20"
    chart_ref: "argo/argo-cd"
    release_namespace: "{{ eks_env }}-devops-argocd-2-0-ns"
    create_namespace: true
    values:
      ...
      server:
        service:
          type: "LoadBalancer"
          loadBalancerSourceRanges:
            ...
        config:
          url: "https://{{ eks_env }}.argocd-2-0.example.com"
          repository.credentials: |
            - url: "https://github.com/projectname/"
              passwordSecret:
                name: github-access
                key: password
              usernameSecret:
                name: github-access
                key: username
...

Ansible ArgoCD Projects и Applications

Создаём каталоги, в которых будем хранить манифесты для проектов и приложений:

mkdir -p roles/argocd/templates/{projects,applications/{backend,web}}

В каталоге roles/argocd/templates/projects/ создаём два манифеста для двух проектов:

vim -p roles/argocd/templates/projects/backend-project.yaml.j2 roles/argocd/templates/projects/web-project.yaml.j2

Описываем проект Backend:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: "backend"
  namespace: "{{ eks_env }}-devops-argocd-2-0-ns"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  description: "Backend project"
  sourceRepos:
  - '*'
  destinations:
  - namespace: "{{ eks_env }}-backend-*"
    server: "https://kubernetes.default.svc"
  clusterResourceWhitelist:
  - group: ''
    kind: Namespace
  namespaceResourceWhitelist:
  - group: "*"
    kind: "*"
  roles:  
  - name: "backend-app-admin"
    description: "Backend team's deployment role"
    policies:
    - p, proj:backend:backend-app-admin, applications, *, backend/*, allow
    groups:
    - "Backend" 
  orphanedResources:
    warn: true

Повторяем для Web.

Аналогично делаем для приложения, пока тут одно тестовое.

Создаём файл roles/argocd/templates/applications/backend/backend-test-app.yaml.j2:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: "backend-app-1"
  namespace: "{{ eks_env }}-devops-argocd-2-0-ns"
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: "backend"
  source:
    repoURL: "https://github.com/projectname/devops-kubernetes.git"
    targetRevision: "DVPS-967-ArgoCD-declarative-bootstrap"
    path: "tests/test-svc"
    helm:
      parameters:
      - name: serviceType
        value: LoadBalancer
  destination:
    server: "https://kubernetes.default.svc"
    namespace: "{{ eks_env }}-backend-argocdapp-test-ns"
  syncPolicy:
    automated:
      prune: true
      selfHeal: false
    syncOptions:
    - Validate=true
    - CreateNamespace=true
    - PrunePropagationPolicy=foreground
    - PruneLast=true
    retry:
      limit: 2

И в конце задачи добавляем применение этих манифестов – сначала создаём проекты, потом приложения:

...
- name: "Create a Backend project"
  community.kubernetes.k8s:
    kubeconfig: "{{ kube_config_path }}"
    state: present
    template: roles/argocd/templates/projects/backend-project.yaml.j2
          
- name: "Create a Web project"
  community.kubernetes.k8s: 
    kubeconfig: "{{ kube_config_path }}"
    state: present
    template: roles/argocd/templates/projects/web-project.yaml.j2
          
- name: "Create Backend applications"
  community.kubernetes.k8s:
    kubeconfig: "{{ kube_config_path }}"
    state: present
    template: "{{ item }}"
  with_fileglob:
    - roles/argocd/templates/applications/backend/*.yaml.j2

- name: "Create Web applications"
  community.kubernetes.k8s:
    kubeconfig: "{{ kube_config_path }}"
    state: present
    template: "{{ item }}"
  with_fileglob:
    - roles/argocd/templates/applications/web/*.yaml.j2

Для Applications используем with_fileglob, что бы выбрать все файлы из каталога roles/argocd/templates/applications/, так как задумывается иметь отдельный файл шаблона для каждого приложения, что бы девелоперам было проще их создавать и обновлять.

Jenkins

См. примеры в Jenkins: миграция RTFM 2.6 – Jenkins Pipeline для Ansible и Helm: пошаговое создание чарта и деплоймента из Jenkins.

Подробно останавливаться не буду, тут всё достаточно просто и для нас стандартно: используем Scripted Pipeline, в которой вызываем функцию provision.ansibleApply():

...
            stage("Applly") {
                // ansibleApply((playbookFile='1', tags='2', passfile_id='3', limit='4')
                provision.ansibleApply( "${PLAYBOOK}", "${env.TAGS}", "${PASSFILE_ID}", "${EKS_ENV}")
            }
...

Сама функция выглядит так:

...
def ansibleApply(playbookFile='1', tags='2', passfile_id='3', limit='4') {
        
    withCredentials([file(credentialsId: "${passfile_id}", variable: 'passfile')]) {

        docker.image('projectname/kubectl-aws:4.5').inside('-v /var/run/docker.sock:/var/run/docker.sock --net=host') {
            
            sh """
                aws sts get-caller-identity
                ansible-playbook ${playbookFile} --tags ${tags} --vault-password-file ${passfile} --limit ${limit}
            """ 
        }   
    }
}.
...

Тут в Docker запускается контейнер с нашим образ с AWS CLI и Ansible, который запускает Ansible playbook, передаёт ему tag, и запускает нужную роль.

И в самом плейбуке для каждой роли заданы теги, которые позволяют выполнять только эту определённую роль:

...
    - role: argocd
      tags: argocd, create-cluster
...

В результате получается Jenkins Job с такими параметрами:

Запускаем:

Логинимся:

argocd login dev-1-18.argocd-2-0.example.com --name admin@dev-1-18.argocd-2-0.example.com

Проверяем приложения:

Готово.