Глава 1: То, что и так видят все, но не каждый готов признать
Новый день, солнце, душ, кофе и инженер садится заниматься своими задачами по созданию или обновлению продукта. В свою очередь пользователь находится по другую сторону баррикад — использует готовый продукт, который упрощает его жизнь. Время показывает, что любой развивающийся бизнес должен идти в ногу со временем. По большей части, суть этой концепции в том, что каждый человек в этой цепочке чего-то хочет. Получается, что быстро придуманная идея — требует быстрой реализации.
В текущих реалиях все IT-продукты разрабатываются с использованием какого-либо ПО, способного управлять репозиториями программного кода для Git. В нашем случае, хотелось бы рассказать про один из самых популярных продуктов Gitlab. «Gitlab — наше всё» должно быть слоганом каждой компании, которая его использует, иначе могут произойти события, которые приведут к печальным последствиям. На Habr можно найти множество различной информации, связанной с кейсами, туториалами или просто интересными историями про GitLab. Но сколько бы ни было написано, найти место где было бы собрано всё и сразу — не получилось. Придется исправлять.
Начнём?
Глава 2: Latest говорит не только о том, что релиз последний, но и о том, что всем вашим релизам может прийти конец
Новый день, солнце, душ, кофе и разработчик садится разбирать эпик. Первое, что он там видит — это деплой в среду тестирования нового функционала. Смело вешая тэг на master ветку, его релиз уверенно падает на стадии build-а образа контейнера. Как такое могло произойти и почему так вышло? Неожиданный результат, опубликованный в рамках канала slack, создаёт отклик в душе других сотрудников компании.
Коллеги, не работают все процессы CI/CD, все релизы и тесты падают с различными ошибками.
Как бы это странно ни звучало, но все Devops-инженеры, изучая проблему, приходят в лёгкое удивление, ведь причина возникновения множества ошибок непонятна. То, что вчера ещё работало — сегодня уже остановило весь процесс разработки.
Лучше всегда всё начинать сначала, этот подход помог и здесь. Ранее при первоначальных настройках различных pipelines во все проектах для стадий build, test и deploy использовался всем известный подход DIND (Docker-in-Docker) и тут нас не может не радовать, что образ для всех job имеет тэг latest.
build_rc:
stage: build
image: docker:dind
Вечерним релизом (накануне проблемы) разработчики продукта docker произвели обновление версии docker в образе dind до 24.0 и все описанные старым способом процессы сборки, тестирования и деплоя начали падать с ошибками docker. Быстрое понимание проблемы помогло найти и решение, изменив текущий tag образа в каждой джобе на 23.0-dind, но и тут возникла весьма интересная проблема, которая говорит об отсутствии детального понимания принципов настройки Gitlab и его раннеров. Образ джобы не был описан глобально, а в индивидуальном варианте для каждого pipeline. «Зачем?» — остается загадкой. В процессе настройки runner-ов каждый имеет свой подход:
Закрепление runners за определенными группами, подгруппами, репозиториями.
Настройка runners для деплоя джоб имеющих соответствующий ему tag.
Подключение runners глобально на уровне панели администратора, предоставляя доступ к нему всем проектам вашего Gitlab.
Если же вы используете именно последний подход, то самым простым и правильным вариантом решения для вас будет настройка образа джобы именно на уровне этого раннера в config.toml:
concurrent = 10
check_interval = 0
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "runner-01.git.test.ru"
url = "https://git.test.ru/"
id = 44
token = "super_secure_token"
token_obtained_at = 2023-07-13T10:24:48Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "docker"
[runners.cache]
MaxUploadedArchiveSize = 0
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "docker:23.0-dind"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
Что получается в итоге?
При отсутствии описания во всех ваших .gitlab-ci.yml образа джобы, по умолчанию будет вызываться необходимый нам образ docker:23.0-dind. В результате подобного инцидента, который произошёл фактически из-за очень маленькой ошибки разработки, трудозатраты DevOps-инженеров на причину понимания проблемы, глобальные правки во всех манифестах .gitlab-ci.yml, которые не имели бы никакого смысла при исходно правильной настройке.
Глава 3: Сохранись, а то мало ли
Тот же день, пекло, обед, сэндвич, латте, yaml манифесты и IaC (Infrastructure-as-Code).
IaC — это очень популярный и полезный подход, так как если мы говорим про Kubernetes, то в первую очередь мы подразумеваем декларативный подход. Соответственно, когда мы планируем использовать Terraform с использованием облачного провайдера для описания нашей инфраструктуры, то наш путь исконно верный. Ещё один интересный кейс, с которым мы столкнулись в схожей проблеме, был завязан вновь на неправильном подходе или настройке Gitlab.
Описав развертывание большого количества виртуальных машин в облаке и параллельно настроив сеть для них, межсетевое взаимодействие, security groups, релиз был успешно запушен в репозиторий, где посредством заранее описанного процесса CI/CD выполнялись стадии validate, plan, apply и сохранение terraform.state в Infrastructure/Terraform.
После успешного отрабатывания всех 3-х стадий, можно фактически убедиться в веб-панели облака о наличии развернутых ресурсов, но каким было удивление, когда было обнаружено, что terraform.state был записан не полностью? Получается, все повторные запуски нашего pipeline при изменении ресурса имели безукоризненное падение, а при анализе вывода успешно отработанных джоб результат посмотреть не удавалось из-за ограниченности вывода количества информации в output.
Как быть и как этого избежать?
Вариантов решения данного вопроса множество, в первую очередь надо не забывать о существующих ограничениях в Gitlab на размер вашего terraform.tfstate:
Во-вторых, не забываем поднимать лимит по количеству строк для gitlab-runners, добавляя output_limit в основной блок:
concurrent = 10
check_interval = 0
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
name = "runner-01.git.test.ru"
url = "https://git.test.ru/"
id = 44
token = "super_secure_token"
token_obtained_at = 2023-07-13T10:24:48Z
token_expires_at = 0001-01-01T00:00:00Z
executor = "docker"
output_limit = 50000000
[runners.cache]
MaxUploadedArchiveSize = 0
[runners.cache.s3]
[runners.cache.gcs]
[runners.cache.azure]
[runners.docker]
tls_verify = false
image = "docker:23.0-dind"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/cache"]
shm_size = 0
В-третьих, очень важным моментом является разделение создаваемых ресурсов Terraform на различные модули. Как пример, вынос создания Network и Security groups в отдельные модули и дальнейшее переиспользование готовых сущностей через data sources для упрощения понимания аспектов управления инфраструктурой и уменьшение исходного tfstate.
Глава 4: Процессы, подходы и слёзы
Самая интересная и самая любимая часть каждого Devops-инженера это, само собой, реализация подхода деплоя и здесь, когда спрашиваешь у коллеги: «А как сделали вы?», возникает целая дискуссия с дебатами и беспрерывными выяснениями, а кто же прав.
Поехали? :)
Каждый любит строить проект и процессы деплоя с нуля, но если вы находитесь на любой из стадий, то обратите сразу внимание на ряд принципиально важных моментов, которые упростят вам жизнь.
Распределяйте все микросервисы по раздельным репозиториям в рамках определенной группы или подгруппы — это упростит предоставления доступов для каждого проекта, а также поможет в реализации использования Gitlab CI/CD variables. Тут же хочется поделиться, что периодически встречается архитектура, когда в рамках одного репозитория лежит 20 каталогов с различными микросервисами, где используются разные подходы и исходные образы для деплоя. «Зачем и почему?» — вопросы хорошие.
-
Определяйте переменные для Gitlab CI/CD пайплайнов глобально в панели администратора или на уровне групп, подгрупп. Согласитесь, дублировать одну и ту же переменную в рамках каждого репозитория — себя не уважать. Особенно будет интересно править их все после изменения переменной или протухания токена.
Отойдите от концепции использования кастомных .gitlab-ci.yml в рамках каждого процесса деплоя. Пишите шаблонизированные .gitlab-ci.yml, подвязываясь под внутренние переменные самого Gitlab, для реализации этого подхода вполне прекрасно подходят include, extends, reference. В дальнейшим их можно с лёгкостью переиспользовать для деплоя других микросервисов:
Глобальный конфиг build:
.build_template: &build
image: nexus.test.ru/generic-images/dind:dind-latest
variables:
DOCKER_HOST: tcp://docker:2375/
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER_NEXUS -p $CI_REGISTRY_PASSWORD_NEXUS $CI_REGISTRY_NEXUS
- docker build --no-cache -t ${CI_REGISTRY_NEXUS}/k8s-${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}:${CI_COMMIT_SHA} .
- docker push ${CI_REGISTRY_NEXUS}/k8s-${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}:${CI_COMMIT_SHA}
Глобальный конфиг deploy:
.deploy_template: &deploy
image:
name: nexus.test.ru/generic-images/k8s-deployer:latest
variables:
HELM_KUBECONTEXT: ${KUBE_CONTEXT}
script:
- helm repo add nixys https://registry.nixys.ru/chartrepo/public
- helm repo update
- helm upgrade
--install ${CI_PROJECT_NAME} nixys/universal-chart
--namespace ${NAMESPACE}
--insecure-skip-tls-verify
--set "defaultImage=${CI_REGISTRY_NEXUS}/k8s-${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}"
--set "defaultImageTag=${CI_COMMIT_SHA}"
--set "secretEnvsString=${VAULT_IMAGES_ENV}"
--create-namespace
--values ${FILE}
--version 2.4.0
--debug
Частный конфиг в виде джоб в pipeline:
include:
- project: ‘test-devops/ci-cd/sample-ci-cd'
ref: main
file: '.build.yml'
- project: 'test-devops/ci-cd/sample-ci-cd'
ref: main
file: '.deploy.yml'
stages:
- build
- deploy
###
# PROD
###
build:
extends: .build_template
stage: build
tags:
- dind
variables:
KUBE_CONTEXT: ${CI_PROJECT_NAMESPACE}/k8s-agents:kubernetes-ya-default-cluster
only:
refs:
- main
deploy-prod:
extends: .deploy_template
when: manual
stage: deploy
tags:
- k8s-runner-prod
needs:
- job: build
variables:
KUBE_CONTEXT: ${CI_PROJECT_NAMESPACE}/k8s-agents:kubernetes-ya-default-cluster
NAMESPACE: "prod"
FILE: ".helm/prod.yml"
environment:
name: production
only:
refs:
- main
###
# DEV
###
deploy-dev:
extends: .deploy_template
when: manual
stage: deploy
tags:
- k8s-runner-dev
needs:
- job: build
variables:
KUBE_CONTEXT: ${CI_PROJECT_NAMESPACE}/k8s-agents:kubernetes-ya-develop-cluster
NAMESPACE: "dev"
FILE: ".helm/dev.yml"
environment:
name: dev
only:
refs:
- develop
Если вы используете Gitlab CI/CD variables и у вас есть переменные, идентичные по названию, но разные по содержанию — всегда используйте Environments.
build dev-kg:
stage: build
services:
- docker:dind
environment:
name: dev-kg
Самое интересное в этой маленькой, но приятной фиче, что у вас есть возможность использовать регулярные выражения при указании переменных в Gitlab, что делает весь этот механизм ещё более простым и комфортным.
Выберите свой подход в процессе Continuous Deployment/Delivery приложений и микросервисов, если вы работаете с kubernetes. На данный момент есть несколько вариантов деплоя.
6.1. Деплой в kubernetes, OLD средствами.
Старое — не значит неработающее. Создаём Namespace, ServiceAccount, Secret, rbac policy и генерируем для него kubeconfig, с помощью которого можно будет безопасно и уверенно совершать деплой в определённый Namespace.
Удобен ли данный вариант? — Вряд ли
Но он помогает детально понимать все аспекты деплоя со стороны безопасности в рамках кластера k8s и опять же в данном способе нет никаких завязок на стороннее ПО. При этом можно с легкостью автоматизировать весь этот процесс, написав Terraform модуль на основе kubernetes provider и gitlab provider. Но если безопасность у вас на данный момент не на первом месте и в целом вы человек прогрессивный, то никто не мешает обратиться к современным подходам.
Перейдём к динозаврам...
6.2. Деплой в kubernetes через Gitlab интеграцию (Kubernetes cluster) — да, да, да.
Для тех кто не знал, расскажу просто и легко. После предоставления ca.crt вашего кластера kubernetes Gitlab-у, вы получаете из группы или подгруппы настроенный деплой из коробки.
Если конкретизировать, то используя ca.crt, Gitlab integration генерирует kubeconfig для деплоя в namespace, добавляя его в виде tmp файла в рамках запущенной джобы вашего pipeline.
Получается все шаги описанные в пункте 1 можно не делать? — да
Помимо упрощения процесса деплоя эта интеграции предоставляет большое количество дополнительных функций. Как пример, при добавлении к deployment annotations можно было получить доступ до shell контейнера прямо из web интерфейса Gitlab.
Но не бывает хороших новостей без плохих, в версии Gitlab 17.0.0 данный функционал возможно уберут и заменят на другой подход реализации процесса интеграции, если ранее процесс деплоя настраивался из Gitlab до K8S через kubeconfig, то теперь процесс работает по принципу взаимодействия:
Gitlab with API Token- > gitlab-agent (предоставляем kubeconfig с правами на деплой)-> kubernetes
6.3. Деплой в kubernetes через Gitlab интеграцию (Kubernetes cluster).
Новый, актуальный, свежий и как уже было написано ранее — безопасный вариант. В настройке он также весьма прост. Делаем инсталляцию helm chart-а готовыми командами, предоставляемыми Gitlab-ом, и создаём конфигурацию в рамках репозитория, в которой мы подключали наш gitlab-agent:
В нашем репозитории необходимо создать схожую структуру с одним yaml конфигом, в рамках которого будут описаны те проекты, которым должны предоставлять возможности деплоя через наш gitlab-agent. Файл лежит по следующему пути и выглядит примерно следующим образом:
.gitlab/agents/kubernetes-ya-default-cluster/config.yaml
ci_access:
projects:
- id: test/test # Project/Repo name to authorize
- id: test/backend
- id: test/websocket
- id: test/static
observability:
logging:
level: debug
Здесь важно понимать, что вам нужно точечно на уровне репозиториев или глобально на уровне группы предоставить доступ для деплоя через gitlab-agent. Важно заметить, что gitlab-agent не может как и ранее выйти за пределы группы Gitlab. Получается в случае, если у вас 2 группы с микросервисами backend и frontend, то вам придётся для одного кластера создавать и деплоить 2 разных агента. Выглядит как минус и лишняя работа, но это маленький шаг в сторону безопасности. Однако нельзя забывать и про существенный минус, с которым можно столкнуться.
Все инженеры любят (и правильно делают), когда обновляют Gitlab — важным моментом здесь является то, что люди, использующие интеграцию для деплоя, неожиданно для себя и для своей команды узнали об уходе в deprecated старого механизма деплоя. Перед обновлением на 17.0.0 версию Gitlab надо будет кардинально подготовиться и переключить все существующие деплои, потеряв часть приятного интерактивного функционала и минусом является даже не это, а то, что вероятнее всего в будущем ситуация повторится и нужно предельно внимательно следить за релизами и это, по сути, является императивной нормой любого продукта.
Обновления — от них не застрахован никто.
Весьма частая и регулярная задача, с которой сталкиваются администраторы и DevOps-инженеры — это обновление Gitlab. Первая ошибка новичка, само собой, обновиться сразу с версии 13 до версии 16.2.1 в 1 шаг. На этот случай Gitlab уже давно разработали весьма удобную площадку, описывающую поэтапность обновления с релизами c вашей версии до последней актуальной. Но самая банальность проблемы заключается в не изучении изменения функционала релиза.
P.S. К ссылочке выше
Используйте VPN, чтобы открыть сайт.
К примеру, все ваши процессы build и deploy контейнера завязаны на использовании вышеописанных глобальных CI/CD переменных:
before_script:
- export RELEASE_VERSION=$(echo $CI_BUILD_REF_NAME | grep -o '[[:digit:]]\{1,5\}\.[[:digit:]]\{1,5\}\.[[:digit:]]\{1,5\}' || echo development)
build_rc:
stage: build
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER_NEXUS -p $CI_REGISTRY_PASSWORD_NEXUS $CI_REGISTRY_NEXUS
- docker build --no-cache -t ${CI_REGISTRY_NEXUS}/k8s-${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}:${RELEASE_VERSION} .
- docker push ${CI_REGISTRY_NEXUS}/k8s-${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}:${RELEASE_VERSION}
В последнем релизе происходит удаление этой переменной или изменения её названия. Результат: все текущие и ранее актуальные pipeline переходят в нерабочий статус, что сразу же грозит простоем разработки и разбором нового инцидента.
Глава 5: Почему не получилось пройти мимо?
Вечер, горячий чай, тёплый ужин и осознание…
Имея под рукой Gitlab, у вас появляется большое количество возможностей и большое количество потенциальных проблем. Регулярные релизы со стороны разработки продукта покрывают оба описанных аспекта ранее, но многое зависит от инженера, реализующего подход как в деплое, так и в поддержке.
Прочитав всё описанное выше, можно сказать, что многие вещи уже рассказаны в разных статьях и мануалах, но каждый раз, подключаясь на проект, видишь одни и те же ошибки или проблемы, которые не могут не огорчать. Порой ошибаются все и никто не безгрешен, но вовремя понять потенциальную проблему, нависшую над текущими рабочими процессами, и исправить её сразу — лучше, чем игнорировать и получать с утра пораньше сообщения о глобальном инциденте всех конвейеров.
Главное — это понять, что правильные решения, видение инфраструктуры и процессов через 6 месяцев, поможет ускорить решение задач как со стороны инженеров, так и со стороны бизнеса. Ведь чем быстрее мы деплоим, тем быстрее растём.
Получается, крепких пайплайнов и быстрых релизов!
Если есть вопросы — пишите в комментариях. И, конечно, подписывайтесь на наш блог Хабр, TG-канал DevOps FM, интернет-издание VC и YouTube — мы всегда рады новым друзьям!
Apoheliy
Сразу видно - автор не потерпит альтернативного мнения.
Сарказм: компиляторы, ide, редакторы, статические анализаторы, библиотеки, санитайзеры, +100500. Не, не слышали!