Как в Kubernetes создать конвейерную обработку CI/CD с автодеплоем c использованием Gitlab и Helm

Недавно я начал работать над несколькими микросервисами Golang.
Я решил прокешировать gitlab и разделить работу на несколько шагов для лучшей обратной связи в пользовательском интерфейсе.

Вот некоторые изменения, появившиеся в моем файле .gitlab-ci.yaml:

  • нет docker-in-docker;
  • использование кеша для программных пакетов вместо готового изображения с зависимостями;
  • разбиение на шаги;
  • автодеплой с помощью Helm.

Построение образа Golang

Поскольку golang привередлив к местоположению проекта, нужно внести некоторые изменения в работу CI. Изменения производятся в блоке before_script, который просто создает необходимые каталоги и исходный код ссылки. Предположительно официальным репозиторием проекта является gitlab.example.com/librerio/libr_files, поэтому сценарий должен выглядеть следующим образом.

variables:
  APP_PATH: /go/src/gitlab.example.com/librerio/libr_files
before_script:
  - mkdir -p /go/src/gitlab.example.com/librerio/
  - ln -s $PWD ${APP_PATH}
  - mkdir -p ${APP_PATH}/vendor
  - cd ${APP_PATH}

Теперь мы можем устанавливать зависимости и строить двоичные файлы. Чтобы избежать загрузки всех пакетов в каждой сборке, нужно настроить кеширование.

Согласно правилам кеширования GitLab нужно добавить каталог поставщика как в кеш, так и в артефакты. Кеш даст возможность использовать каталог между заданиями сборки, а программные изделия — внутри того же задания.

cache:
  untracked: true
  key: "$CI_BUILD_REF_NAME"
  paths:
    - vendor/
setup:
  stage: setup
  image: lwolf/golang-glide:0.12.3
  script:
    - glide install -v
  artifacts:
    paths:
     - vendor/

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

build:
  stage: build
  image: lwolf/golang-glide:0.12.3
  script:
    - cd ${APP_PATH}
    - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o release/app -ldflags '-w -s'
    - cd release
  artifacts:
    paths:
     - release/

Этап тестирования

Чтобы запустить тесты golang с отчетами о покрытии, я использую скрипт оболочки. Он выполняет все тесты в подкаталогах проекта и создает отчет о покрытии. Прежде чем опубликовать скрипт в gist, я немного изменил его, исключив каталог поставщика из тестов.

покрытие regexp для gitlab-ci: ^total:\s*\(statements\)\s*(\d+.\d+\%)

Этап развертывания

Я не использую встроенную интеграцию Gitlab с Kubernetes.

Сначала я думал о создании Kubernetes-секретов и проведении мониторинга в под gitlab-runner. Но это сложно: развертывание необходимо обновлять каждый раз, когда требуется добавить новую конфигурацию кластера. Поэтому я использую переменные CI/CD GitLab с конфигурацией Kubernetes и кодировкой base64. Каждый проект может иметь любое количество конфигураций. Процесс прост: создайте строку base64 из конфига и скопируйте его в буфер обмена. После этого поместите его в переменную kube_config (назовите ее как угодно).

cat ~/.kube/config | base64 | pbcopy

Если вы не обладаете полной установкой gitlab, подумайте о создании пользователя Kubernetes с ограниченными разрешениями.

Затем на этапе развертывания можно декодировать KUBECONFIGпеременную обратно в файл и использовать ее с kubectl.

variables:
  KUBECONFIG: /etc/deploy/config
deploy:
  ...
  before_script:
    - mkdir -p /etc/deploy
    - echo ${kube_config} | base64 -d > ${KUBECONFIG}
    - kubectl config use-context homekube
    - helm init --client-only
    - helm repo add stable https://kubernetes-charts.storage.googleapis.com/
    - helm repo add incubator https://kubernetes-charts-incubator.storage.googleapis.com/
    - helm repo update

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

Например, у вас есть две версии API: v1.0 и v1.1. Все, что нужно сделать, — установить appVersion в файле Chart.yaml. Система сборки проверяет версии API и развертывает или обновляет необходимую.

- export API_VERSION="$(grep "appVersion" Chart.yaml | cut -d" " -f2)"
- export RELEASE_NAME="libr-files-v${API_VERSION/./-}"
- export DEPLOYS=$(helm ls | grep $RELEASE_NAME | wc -l)
- if [ ${DEPLOYS}  -eq 0 ]; then helm install --name=${RELEASE_NAME} . --namespace=${STAGING_NAMESPACE}; else helm upgrade ${RELEASE_NAME} . --namespace=${STAGING_NAMESPACE}; fi

TL;DR;

Вот полный файл .gitlab-ci.yaml.

cache:
  untracked: true
  key: "$CI_BUILD_REF_NAME"
  paths:
    - vendor/
before_script:
  - mkdir -p /go/src/gitlab.example.com/librerio/
  - ln -s $PWD ${APP_PATH}
  - mkdir -p ${APP_PATH}/vendor
  - cd ${APP_PATH}
stages:
  - setup
  - test
  - build
  - release
  - deploy
variables:
  CONTAINER_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_PATH}:${CI_BUILD_REF_NAME}_${CI_BUILD_REF}
  CONTAINER_IMAGE_LATEST: ${CI_REGISTRY}/${CI_PROJECT_PATH}:latest
  DOCKER_DRIVER: overlay2
  KUBECONFIG: /etc/deploy/config
  STAGING_NAMESPACE: app-stage
  PRODUCTION_NAMESPACE: app-prod
  APP_PATH: /go/src/gitlab.example.com/librerio/libr_files
  POSTGRES_USER: gorma
  POSTGRES_DB: test-${CI_BUILD_REF}
  POSTGRES_PASSWORD: gorma
setup:
  stage: setup
  image: lwolf/golang-glide:0.12.3
  script:
    - glide install -v
  artifacts:
    paths:
     - vendor/
build:
  stage: build
  image: lwolf/golang-glide:0.12.3
  script:
    - cd ${APP_PATH}
    - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o release/app -ldflags '-w -s'
    - cd release
  artifacts:
    paths:
     - release/
release:
  stage: release
  image: docker:latest
  script:
    - cd ${APP_PATH}/release
    - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
    - docker build -t ${CONTAINER_IMAGE} .
    - docker tag ${CONTAINER_IMAGE} ${CONTAINER_IMAGE_LATEST}
    - docker push ${CONTAINER_IMAGE}
    - docker push ${CONTAINER_IMAGE_LATEST}
test:
  stage: test
  image: lwolf/golang-glide:0.12.3
  services:
    - postgres:9.6
  script:
    - cd ${APP_PATH}
    - curl -o coverage.sh https://gist.githubusercontent.com/lwolf/3764a3b6cd08387e80aa6ca3b9534b8a/raw
    - sh coverage.sh
deploy_staging:
  stage: deploy
  image: lwolf/helm-kubectl-docker:v152_213
  before_script:
    - mkdir -p /etc/deploy
    - echo ${kube_config} | base64 -d > ${KUBECONFIG}
    - kubectl config use-context homekube
    - helm init --client-only
    - helm repo add stable https://kubernetes-charts.storage.googleapis.com/
    - helm repo add incubator https://kubernetes-charts-incubator.storage.googleapis.com/
    - helm repo update
  script:
    - cd deploy/libr-files
    - helm dep build
    - export API_VERSION="$(grep "appVersion" Chart.yaml | cut -d" " -f2)"
    - export RELEASE_NAME="libr-files-v${API_VERSION/./-}"
    - export DEPLOYS=$(helm ls | grep $RELEASE_NAME | wc -l)
    - if [ ${DEPLOYS}  -eq 0 ]; then helm install --name=${RELEASE_NAME} . --namespace=${STAGING_NAMESPACE}; else helm upgrade ${RELEASE_NAME} . --namespace=${STAGING_NAMESPACE}; fi
  environment:
    name: staging
    url: https://librerio.example.com
  only:
  - master