Будуємо динамічний environment за допомогою Kubernetes та Gitlab CI

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Вітаю спільноту DOU! Мене звати Юлій, я DevOps в UNI-OPS. Хочу розповісти про цікавий кейс з побудови динамічного environment за допомогою Kubernetes та Gitlab CI.

Як нам це допоможе? Дуже часто ми маємо проблему та постійну необхідність в тестуванні нових функцій і окремому середовищі для розробки. Найчастіше це призводить до запитів DevOps-команди щодо налагодження нового середовища або робить процес розробки поступовим. Тому є гарне рішення, яке дозволить зняти зайвий час на підняття нових ресурсів та налагодження їх під необхідні параметри та умови.

У нас є дуже корисні інструменти для побудови такої інфраструктури, яка б дозволила розробникам самотужки зробити деплоймент потрібної гілки та перевірити поточний код без потреби вимагати окремий сервер у DevOps-команди. Наша інфраструктура буде працює за допомогою Gitlab CI, Kubernetes та Cloudflare.

Процес вимагає від розробника лише зробити коміт у потрібну гілку та отримати URL по закінченню CI за декілька хвилин.

Схема базується на окремих гілках. Є «Main» та «Develop» — основні гілки для розробки, а ще використовуємо «feature/» гілки для впровадження нових функцій або тестування нових інструментів на проєкті.

Налаштування Kubernetes

Почнемо з додавання потрібних плагінів до Kubernetes.

Для цього нам знадобиться external-dns. Він створений для автоматичного додавання DNS A records до DNS-менеджера. Він працює як з Cloudflare, який ми сьогодні використовуємо, так і з багатьма іншими платформами (AWS route 53, Google Cloud DNS, Hetzner, OVH etc.).

У цьому helm chart нам потрібно змінити ресурси, з якими ми будемо працювати (у нашому кейсі — це ingress):

sources:

# - crd
# - service
- ingress
# - contour-httpproxy

Та шукаємо потрібного провайдера:

provider: cloudflare

Далі прописуємо наш API-ключ від Cloudflare. Надаємо потрібні доступи до доменів, якими він може керувати. Його можливо створити за цією адресою.

Далі — завантажуємо до Kubernetes цей плагін:

## Cloudflare configuration to be set via arguments/env. variables
##
cloudflare:
## @param cloudflare.apiToken When using the Cloudflare provider, `CF_API_TOKEN` to set (optional) ##
apiToken: ""
## @param cloudflare.apiKey When using the Cloudflare provider, `CF_API_KEY' to set (optional) ##

Також можливо додати nginx-ingress controller для більш детального налаштування ingress та керування доступом до хостів.

Ще можливо додати Kubernetes Janitor. Він допоможе нам прибирати ресурси, які вийшли по часу. Наприклад, можна створити правило для кожного namespace або deployments, де він буде жити лише обмежену кількість часу. Додається Annotation до ресурсу janitor/ttl=24h.

Все. У нас налаштована автоматизація на ingress-ресурс та можна йти далі.

Репозиторії GitLab

Ми маємо у проєкті gitlab-ci.yml файл, у котрому є кроки з лінтером, unit-test, білдом Docker-образа та деплоєм у Kubernetes:

stages:
  - linting
  - test
  - build
  - deploy-develop
  - deploy-features
  - deploy-prod
variables:
  DOCKER_DRIVER: overlay2
  TAG: "$CI_COMMIT_SHORT_SHA"
  BRANCH: "$CI_COMMIT_REF_SLUG"
.default_rules:
  rules:
     - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  when: never
linter:
  stage: linting
  image: registry.gitlab.com/pipeline-components/jsonlint:latest
  script:
     - |
       find . -not -path './.git/*' -name '*.json' -type f -print0 |
       parallel --will-cite -k -0 -n1 jsonlint -q
  rules:
     - !reference [.default_rules, rules]
     - if: $CI_COMMIT_BRANCH
unit-test:
  stage: test
  image: node:20-alpine
  script:
     - npm install && npm cache clean --force
     - ./node_modules/.bin/jest --ci --reporters=default --reporters=jest-junit
  artifacts:
     when: always
     reports:
       junit:
         - junit.xml
  rules:
    - !reference [.default_rules, rules]
    - if: $CI_COMMIT_BRANCH
Build:
  stage: build
  image: bitnami/kaniko:1.23.0
  script:
    - /kaniko/executor --context "${CI_PROJECT_DIR}" --dockerfile "${CI_PROJECT_DIR}/Dockerfile" --destination "${CI_REGISTRY_IMAGE}:$CI_COMMIT_SHORT_SHA"
  rules:
     - !reference [.default_rules, rules]
     - if: $CI_COMMIT_BRANCH
  environment:
     name: $BRANCH
deploy-prod:
  stage: deploy-prod
  variables:
     BRANCH: "$CI_COMMIT_REF_SLUG"
     TAG: "${CI_COMMIT_SHORT_SHA}"
  trigger:
    project: devops/helm/example-helm-build-app
  when: manual
  only:
     - master
deploy-features:
  stage: deploy-features
  variables:
      BRANCH: "$CI_COMMIT_REF_SLUG"
      TAG: "${CI_COMMIT_SHORT_SHA}"
  trigger:
     project: devops/helm/example-helm-build-app
  rules:
     - !reference [.default_rules, rules]
     - if: '$CI_COMMIT_BRANCH =~ /^feature.*$/'
  when: manual
deploy-develop:
  stage: deploy-develop
  variables:
     BRANCH: "$CI_COMMIT_REF_SLUG"
     TAG: "${CI_COMMIT_SHORT_SHA}"
  trigger:
     project: devops/helm/example-helm-build-app
  rules:
    - !reference [.default_rules, rules]
    - if: '$CI_COMMIT_BRANCH == "develop"'

На виході в нас буде побудований наступний пайплайн, у котрому ми маємо downstream job deploy-features:

Схема, що ілюструє, як налаштований зв’язок між репозиторіями:

Репозиторій з Helm Chart

У репозиторії з Helm ми маємо стандартний helm chart та свій gitlab-ci.yml

gitlab-ci.yml:

stages:
 - linting
 - Deploy
 - Cleanup
variables:
 DOCKER_DRIVER: overlay2
 TAG: "$CI_COMMIT_SHORT_SHA"
 BRANCH: ${BRANCH}
linter:
 stage: linting
 image: registry.gitlab.com/pipeline-components/jsonlint:latest
 script:
   - |
     find . -not -path './.git/*' -name '*.json' -type f -print0 |
     parallel --will-cite -k -0 -n1 jsonlint -q
 only:
   - branches
Deploy:
 stage: Deploy
 image: alpine/helm
 script:
   - mkdir ~/.kube
   - echo "$CI_KUBECONFIG" > ~/.kube/config
   - export KUBECONFIG=~/.kube/config
   - helm upgrade --install --namespace $BRANCH --create-namespace $CI_PROJECT_NAME  . --set "image.tag=$TAG" --set "ingress.hosts[0].host=$CI_PROJECT_NAME-$BRANCH.example.com,ingress.hosts[0].paths[0].path=/,ingress.hosts[0].paths[0].pathType=ImplementationSpecific"
 environment:
   name: $CI_PROJECT_NAME-$BRANCH
   url: https://$CI_PROJECT_NAME-$BRANCH.example.com
   on_stop: stop_env
stop_env:
 stage: Cleanup
 image: alpine/helm
 script:
   - mkdir ~/.kube
   - echo "$CI_KUBECONFIG" > ~/.kube/config
   - export KUBECONFIG=~/.kube/config
   - helm uninstall --namespace $BRANCH  $CI_PROJECT_NAME
 environment:
   name: $CI_PROJECT_NAME-$BRANCH
   url: https://$CI_PROJECT_NAME-$BRANCH.example.com
   action: stop
 when: manual

Для створення динаміки нам потрібен параметр

-set "image.tag=$TAG" --set "ingress.hosts[0].host=$CI_PROJECT_NAME-$BRANCH.example.com,ingress.hosts[0].paths[0].path=/,ingress.hosts[0].paths[0].pathType=ImplementationSpecific"

Зверніть увагу: на старих версіях ingress та Kubernetes <=1.19-0 синтаксис до тіла ingress матиме інакший вигляд. Перевірити це можливо в ingres.yml файлі "apiVersion: networking.k8s.io/v1".

Тепер ми маємо повністю робочий процес, в якому розробник може зробити гілку свою та отримати домен після виповнення всього CI.

Release "example-app" has been upgraded. Happy Helming!
NAME: example-app
NAMESPACE: develop
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands: https://example-app-ticket-53.example.com
Cleaning up project directory and file based variables
00:00
Job succeeded

Розробник бачить у GitLab після виконання CI посилання до свого проєкту.

Дякую за увагу. Сподіваюсь, вам була цікавою та корисною ця стаття. Відповім на всі питання у коментарях!

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному6
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Доречі щоб не бавитись з external-dns, можна просто зробити сабдомен типу `dev.company.name`, і один вайлдкард рекорд `*.dev.company.name` на кластерний інгрес (наприклад глобальний сервіс traefik, і інгреси в кожному неймспейсі).

Скільки у вас всього сервісів і скільки із них задеплоїно одночасно в одне оточення?

Якщо в одному оточенні є лише один або кілька сервісів, чи робите ви дзеркалювання трафіку з апстрім сервісів на той, що біжить у фіча-енвайрменті і якщо робите, то як?

Процес вимагає від розробника лише зробити коміт у потрібну гілку та отримати URL по закінченню CI за декілька хвилин.

Більш актуальним є питання clean-up таких енвайроментів. Та питання обмеження їх кількості для кожного розробника. Реалізувавши динамічні енви, ми стикнулися з цими питаннями наступного місяця, як отримали білл від Амазона на послуги :-)

IMO, це виглядає як машинний переклад сніппета з medium.

1) Як ви працюєте з secrets, як applications отримують credentials до баз даних тощо?
2) Як ви убезпечуєте prod від стирання, prod — це окремий кластер чи ви все це в одному кластері запускаєте? Як ми стерли PROD

1 — Слушне питання. Ізоляція енвайроментів має бути повною, інакше проблема з базами неминуча — ми деплоїли мікросервіси з базами в кожен динамічний енв замість щоб використовувати RDS для них всіх — це було дорожче по ресурсам, але простіше з точки зору адміністрування. В той саме час це ставило певні обмеження на використання динамічних енвайроменів: якщо Оракл ще можна засунути в мікросервіс, то Aurora чи Dynamo — ніяк. Відповідно, creds для баз даних у варіанті база-як-мікросервіс генерується під час виконання пайплайну. Всі інші creds до інших шарених сервісів використовуються однакові з одного сету creds для dev-енвайроменту. Але тут треба не забувати, що кожен динамічний енв може писати в сервіс, і було б непогано відразу налагодити процедуру clean-up для таких даних — ми стикнулися з тим, що такий шарений ресурс стає дуже dirty через короткий проміжок часу і спливає проблема з великою кількістю непотрібних даних, інколи, навіть невалідних даних (ох ці тестери!)

2 — Prod та Dev має бути завжди на окремих кластерах. Це золоте правило, яке дозволяє спати спокійно.

Підписатись на коментарі