Kubernetes를 사용하는 GitLab CI의 JUnit

소프트웨어 테스트가 중요하고 필요하다는 사실을 모두가 잘 알고 있으며 많은 사람들이 오랫동안 자동으로 테스트를 수행해 왔음에도 불구하고 Habr의 광대한 지역에서는 이러한 인기 제품의 조합을 설정하는 단일 방법이 없었습니다. 이 틈새 시장은 (우리가 가장 좋아하는) GitLab 및 JUnit입니다. 이 공백을 메우자!

Kubernetes를 사용하는 GitLab CI의 JUnit

입문

먼저 몇 가지 맥락을 설명하겠습니다.

  • 모든 애플리케이션은 Kubernetes에서 실행되므로 적절한 인프라에서 테스트를 실행하는 것을 고려할 것입니다.
  • 조립 및 배포를 위해 우리는 워프 (인프라 구성 요소 측면에서 이는 자동으로 Helm이 관련되어 있음을 의미합니다).
  • 실제 테스트 생성에 대한 자세한 내용은 다루지 않겠습니다. 우리의 경우 클라이언트가 테스트를 직접 작성하고 테스트 시작(및 병합 요청에 해당 보고서가 있는지)만 확인합니다.


일반적인 작업 순서는 어떻게 되나요?

  1. 애플리케이션 구축 - 이 단계에 대한 설명은 생략하겠습니다.
  2. Kubernetes 클러스터의 별도 네임스페이스에 애플리케이션을 배포하고 테스트를 시작합니다.
  3. GitLab을 사용하여 아티팩트를 검색하고 JUnit 보고서를 구문 분석합니다.
  4. 이전에 생성된 네임스페이스를 삭제합니다.

이제 구현에 들어갑니다!

조정

깃랩 CI

조각부터 시작해보자 .gitlab-ci.yaml, 애플리케이션 배포 및 테스트 실행을 설명합니다. 목록이 꽤 방대해져서 주석으로 철저하게 보완되었습니다.

variables:
# объявляем версию werf, которую собираемся использовать
  WERF_VERSION: "1.0 beta"

.base_deploy: &base_deploy
  script:
# создаем namespace в K8s, если его нет
    - kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# загружаем werf и деплоим — подробнее об этом см. в документации
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#deploy-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf deploy --stages-storage :local
      --namespace ${CI_ENVIRONMENT_SLUG}
      --set "global.commit_ref_slug=${CI_COMMIT_REF_SLUG:-''}"
# передаем переменную `run_tests`
# она будет использоваться в рендере Helm-релиза
      --set "global.run_tests=${RUN_TESTS:-no}"
      --set "global.env=${CI_ENVIRONMENT_SLUG}"
# изменяем timeout (бывают долгие тесты) и передаем его в релиз
      --set "global.ci_timeout=${CI_TIMEOUT:-900}"
     --timeout ${CI_TIMEOUT:-900}
  dependencies:
    - Build

.test-base: &test-base
  extends: .base_deploy
  before_script:
# создаем директорию для будущего отчета, исходя из $CI_COMMIT_REF_SLUG
    - mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# вынужденный костыль, т.к. GitLab хочет получить артефакты в своем build-dir’е
    - mkdir ./tests || true
    - ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
  after_script:
# после окончания тестов удаляем релиз вместе с Job’ом
# (и, возможно, его инфраструктурой)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf dismiss --namespace ${CI_ENVIRONMENT_SLUG} --with-namespace
# мы разрешаем падения, но вы можете сделать иначе
  allow_failure: true
  variables:
    RUN_TESTS: 'yes'
# задаем контекст в werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
    WERF_KUBE_CONTEXT: 'admin@stage-cluster'
  tags:
# используем раннер с тегом `werf-runner`
    - werf-runner
  artifacts:
# требуется собрать артефакт для того, чтобы его можно было увидеть
# в пайплайне и скачать — например, для более вдумчивого изучения
    paths:
      - ./tests/${CI_COMMIT_REF_SLUG}/*
# артефакты старше недели будут удалены
    expire_in: 7 day
# важно: эти строки отвечают за парсинг отчета GitLab’ом
    reports:
      junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml

# для упрощения здесь показаны всего две стадии
# в реальности же у вас их будет больше — как минимум из-за деплоя
stages:
  - build
  - tests

build:
  stage: build
  script:
# сборка — снова по документации по werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#build-stage)
    - type multiwerf && source <(multiwerf use ${WERF_VERSION})
    - werf version
    - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
    - werf build-and-publish --stages-storage :local
  tags:
    - werf-runner
  except:
    - schedules

run tests:
  <<: *test-base
  environment:
# "сама соль" именования namespace’а
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
    name: tests-${CI_COMMIT_REF_SLUG}
  stage: tests
  except:
    - schedules

Kubernetes

이제 디렉토리에 .helm/templates Job을 사용하여 YAML을 생성해 보겠습니다. tests-job.yaml — 테스트와 필요한 Kubernetes 리소스를 실행합니다. 목록을 작성한 후 설명을 참조하세요.

{{- if eq .Values.global.run_tests "yes" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: tests-script
data:
  tests.sh: |
    echo "======================"
    echo "${APP_NAME} TESTS"
    echo "======================"

    cd /app
    npm run test:ci
    cp report.xml /app/test_results/${CI_COMMIT_REF_SLUG}/

    echo ""
    echo ""
    echo ""

    chown -R 999:999 /app/test_results/${CI_COMMIT_REF_SLUG}
---
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Chart.Name }}-test
  annotations:
    "helm.sh/hook": post-install,post-upgrade
    "helm.sh/hook-weight": "2"
    "werf/watch-logs": "true"
spec:
  activeDeadlineSeconds: {{ .Values.global.ci_timeout }}
  backoffLimit: 1
  template:
    metadata:
      name: {{ .Chart.Name }}-test
    spec:
      containers:
      - name: test
        command: ['bash', '-c', '/app/tests.sh']
{{ tuple "application" . | include "werf_container_image" | indent 8 }}
        env:
        - name: env
          value: {{ .Values.global.env }}
        - name: CI_COMMIT_REF_SLUG
          value: {{ .Values.global.commit_ref_slug }}
       - name: APP_NAME
          value: {{ .Chart.Name }}
{{ tuple "application" . | include "werf_container_env" | indent 8 }}
        volumeMounts:
        - mountPath: /app/test_results/
          name: data
        - mountPath: /app/tests.sh
          name: tests-script
          subPath: tests.sh
      tolerations:
      - key: dedicated
        operator: Exists
      - key: node-role.kubernetes.io/master
        operator: Exists
      restartPolicy: OnFailure
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: {{ .Chart.Name }}-pvc
      - name: tests-script
        configMap:
          name: tests-script
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ .Chart.Name }}-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Mi
  storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
  volumeName: {{ .Values.global.commit_ref_slug }}

---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: {{ .Values.global.commit_ref_slug }}
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 10Mi
  local:
    path: /mnt/tests/
  nodeAffinity:
   required:
     nodeSelectorTerms:
     - matchExpressions:
       - key: kubernetes.io/hostname
         operator: In
         values:
         - kube-master
  persistentVolumeReclaimPolicy: Delete
  storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
{{- end }}

어떤 종류의 자원 이 구성에 설명되어 있습니까? 배포할 때 프로젝트에 대한 고유한 네임스페이스를 생성합니다(이는 .gitlab-ci.yaml - tests-${CI_COMMIT_REF_SLUG}) 이를 롤아웃합니다.

  1. 컨피그맵 테스트 스크립트로;
  2. 포드에 대한 설명과 지정된 지시문 포함 command, 테스트만 실행합니다.
  3. PV 및 PVC, 테스트 데이터를 저장할 수 있습니다.

입문 조건에 주의하세요. if 매니페스트 시작 부분에 - 따라서 애플리케이션이 포함된 Helm 차트의 다른 YAML 파일을 래핑해야 합니다. 뒤집다 테스트 중에 배포되지 않도록 설계하세요. 그건:

{{- if ne .Values.global.run_tests "yes" }}
---
я другой ямлик
{{- end }}

그러나 테스트를 하면 일부 인프라가 필요함 (예: Redis, RabbitMQ, Mongo, PostgreSQL...) - YAML은 다음과 같을 수 있습니다. 아니 끄다. 테스트 환경에도 배포합니다. 물론 적절하다고 생각되는 대로 조정합니다.

최종 터치

왜냐하면 werf를 사용한 조립 및 배포는 여전히 작동 중입니다. 빌드 서버(gitlab-runner 사용)에서 테스트가 포함된 포드가 마스터에서 실행되면 디렉터리를 생성해야 합니다. /mnt/tests 주인에게 달려가서 주자에게 주고, 예를 들어 NFS를 통해. 설명이 포함된 자세한 예는 다음에서 확인할 수 있습니다. K8s 문서.

결과는 다음과 같습니다:

user@kube-master:~$ cat /etc/exports | grep tests
/mnt/tests    IP_gitlab-builder/32(rw,nohide,insecure,no_subtree_check,sync,all_squash,anonuid=999,anongid=998)

user@gitlab-runner:~$ cat /etc/fstab | grep tests
IP_kube-master:/mnt/tests    /mnt/tests   nfs4    _netdev,auto  0       0

gitlab-runner에서 직접 NFS 공유를 만든 다음 이를 포드에 마운트하는 것을 금지하는 사람은 없습니다.

주의

쉘 실행기에서 직접 테스트를 사용하여 스크립트를 실행할 수 있는데 왜 Job을 생성하여 모든 것을 복잡하게 만드는지 궁금할 것입니다. 대답은 아주 사소한 것입니다 ...

일부 테스트에서는 올바르게 작동하는지 확인하기 위해 인프라(MongoDB, RabbitMQ, PostgreSQL 등)에 대한 액세스가 필요합니다. 우리는 테스트를 통합했습니다. 이 접근 방식을 사용하면 그러한 추가 엔터티를 포함하는 것이 쉬워집니다. 이 외에도 우리는 표준 배포 접근 방식(NFS를 사용하는 경우에도 디렉터리 추가 마운트)

결과

준비된 구성을 적용하면 무엇을 보게 될까요?

병합 요청에는 최신 파이프라인에서 실행되는 테스트에 대한 요약 통계가 표시됩니다.

Kubernetes를 사용하는 GitLab CI의 JUnit

자세한 내용을 보려면 여기에서 각 오류를 클릭하세요.

Kubernetes를 사용하는 GitLab CI의 JUnit

NB: 세심한 독자라면 우리가 NodeJS 애플리케이션을 테스트하고 있다는 사실을 알아차릴 것이며 스크린샷에서는 .NET... 놀라지 마십시오. 기사를 준비하는 동안 첫 번째 애플리케이션을 테스트할 때 오류가 발견되지 않았지만 그들은 다른 곳에서 발견되었습니다.

결론

보시다시피 복잡한 것은 없습니다!

원칙적으로 이미 셸 수집기가 있고 작동하지만 Kubernetes가 필요하지 않은 경우 여기에 테스트를 연결하는 것은 여기에 설명된 것보다 훨씬 간단한 작업입니다. 그리고 GitLab CI 문서 Ruby, Go, Gradle, Maven 등의 예를 찾을 수 있습니다.

PS

블로그에서도 읽어보세요.

출처 : habr.com

코멘트를 추가