Skip to content

Instantly share code, notes, and snippets.

@mjr27
Created May 5, 2020 14:06
Show Gist options
  • Save mjr27/908dd5281db706fac792950c1d73d37c to your computer and use it in GitHub Desktop.
Save mjr27/908dd5281db706fac792950c1d73d37c to your computer and use it in GitHub Desktop.

Gitlab на пальцах

Глоссарий

  • Артефакт -- результат сборки проекта. Папка dist/, собранный exe, rpm, или докер образ
  • CI - непрерывная интеграция. По каждому чиху на сервере делаем билды, гоняем тесты, собираем метрики всякие, рапортуем о проблемах. И, как финальная стадия, создаем артефакты и сохраняем артефакты в хранилище.
  • CD - непрерывная доставка. Автоматическая или автоматизированная публикация артефактов куда-то там. На прод, на стейджинг, на фтп, в аппстор.
  • прод - под этим далее буду подразумевать собственно место, куда выливается сайт. это может быть стейджинг, демо и т д - не суть важно

Базовые понятия

Цель всего этого мероприятия -- минимизировать геморрой при выливке апдейтов на сервер. Типа лучше день потерять, потом за пять минут долететь.

Попытки минимизировать геморрой заключаются в следующем:

  • раннее выявление ошибок: лучше, чтобы проект не собрался на билд сервере, чем на проде
  • минимальное человеческое участие: то, к чему человек не прикасается, он запороть не может.
  • воспроизводимость: все собирается только на билд сервере. Версия 1.2.1 в хранилище артефактов побайтно соответствует тому, что на проде. Можно брать и щупать. "УМВР" уходит в прошлое (в разумных пределах, да)

Сейчас модно разделять собственно это все на этапы CI и CD. То есть вначале готовим и сохраняем, а потом уже копируем сохраненное на прод.

CI

В базовом виде представляет из себя что-то типа

var repo = "https://github.com/user/repo";
while(true) {
    if (!hasNewCommits(repo)) continue;
    cloneGit(repo);
    var result = build();
    if (result.success) {
        save(result.artefact);
    } else {
        console.error("Билд запортачен")
    }
    sleep(1);
}

По факту, основная цель -- убедиться, что коммит (билд, мердж реквест, тэг) не говно.

В реальной жизни

В реальной жизни последний несколько лет все сводится к:

  • прогнать тесты
  • собрать докер контейнеры (практически всегда ровно один)
  • запушить их в docker registry

Причем одни считают, что тесты нужно гонять уже на собранных контейнерах (в условиях, максимально приближенных к боевым). Другие же, что тестам со всеми их зависимостями на проде делать нечего.

По мне, похрен. Главное, чтобы тесты были и показывали, где что сломалось.

CD

Собственно, задача этой системы -- взять результаты билда и запустить где-то там. Звучит просто. В реальности все немного хуже.

Для начала, есть два конкурирующих подхода push и pull.

  • PUSH -- привет из 1998 года. Разработчик заходит на прод по FTP и копирует файлы.
  • PULL -- привет из 2008 года. git pull && composer update в кронтабе.

Оно, конечно, обновилось за прошедшие годы, но не принципиально.

Push

Все сводится к тому, что на билд-сервере запускается команда, обновляющая прод.

Ключевые слова:

  • ansible - можно сказать, отраслевой стандарт
  • Deployer -- то же самое, только на пхп
  • Занятно, что зайти скриптом на сервер по ssh и сделать git pull / docker-compose pull - это на удивление тоже push
  • Для кубера это разрекламированный на хабре Werf

Для кубера это обычно сводится к kubectl set image deployment/nginx-deployment nginx=nginx:1.16.1 --record или helm update nginx@1.16.1

Плюсы очевидные

  • это реально просто и понятно. По факту, доверяешь скрипту ту работу, которую привык делать сам.
  • от сервера (обычно) нужен только открытый ssh порт.

Минусы более тонкие:

  • на билд сервере нужно где-то сохранить доступ к проду.

  • Как только сервера становится два, появляется целый ряд организационных вопросов. Самый частый -- что делать, если один сервер обновился, а второй почему-то нет.

Pull

Сервер периодически (или по хуку) проверяет новое состояние и применяет изменения.

Известные товарищи:

  • chef / puppet предки всех современных модных слов
  • watchtower автообновление образов докера.
  • flux примерно то же самое для кубера.

Минусы:

  • На проде нужен какой-то агент, который следит за обновлениями. Его нужно устанавливать, конфигурировать и обслуживать. А конфигурить это обычно ад еще тот.
  • Изменения обычно применяются не мгновенно. Теоретически возможен временный рассинхрон между серверами прода.

Плюсы:

  • Изменения рано или поздно, но приползут.
  • Доступ к проду можно контролировать.

Сравнение подходов

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

Gitlab

И вот мы, собственно, переходим к самой мякотке.

Гитлаб, собственно, предоставляет только CI, но при желании CD можно тоже туда впихнуть.

Для работы гитлабу нужно несколько виртуалок, где собственно будут выполняться задачи. Для этого там крутятся gitlab runners. Вопросы их конфигурации оставим за кадром (оно есть в документации), важно понимать, что машин может быть несколько и что каждая задача совсем не обязательно будет работать на той же машине. что и предыдушая.

Далее несколько простыней на примере сборки нодовского приложения.

Создаем файл конфигурации

В корне проекта создаем новый .gitlab-ci.yml.

Это собтвенно файл, описывающий последовательность шагов для сборки проекта.

Пишем в него

stages:
- test
- build
- deploy

У нас будет три стадии сборки: прогоняем тесты, потом собираем докер образ, а потом выливаем его... куда-нибудь.

Начнем с тестов.

Что там нам для этого нужно? npm install && npm test? Мы можем поставить ноду прямо на машины с раннерами, но лень, поэтому будем это делать в докере так и пишем

stages:
- test
- build
- deploy

test:
  stage: test
  image: node:12
  script:
  - npm install
  - npm test

Сохраняем, файл, коммитимся, идем в гитлаб, видим что там что-то крутится и закончилось успешно. Или нет.

Идем дальше. Мы хотим зафигачить это в контейнер.

Что там мы делаем? npm build, после чего фронт у нас в папочке dist/? Окей

Делаем Dockerfile в корне проекта:

FROM node:12 as base # временный контейнер. Мы в нем соберем фронт и выкинем
RUN npm install  
RUN npm build # собрали
FROM nginx # и начинаем с какого-то мелкого образа без лишней хрени.
COPY --from=base dist /var/www/html # копируем из предыдушего образа только то, что нужно

Протестили на локали, вроде работает

Идем дальше

У каждого репо в гитлабе есть свой собственный docker registry.

stages:
- test
- build
- deploy

test:
  stage: test
  image: node:12
  script:
  - npm install
  - npm test

build: # мы не указываем image. предполагаем, что это все запускается
       # просто в шелле на машине с раннером, без всяких докеров
       # https://hub.docker.com/_/docker - это уже злая рекурсия
  stage: build
  before_script:
  # это предопредленнные перменные, чтобы залогиниться в docker registry
  # из gitlab ci https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
  - docker login -u ${CI_JOB_USERNAME} -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
  script:
  # билдим докерфайл с двумя тегами dev-(номерКоммита) и latest
  - docker build -f ./Dockerfile ./ -t ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA} -t ${CI_REGISTRY_IMAGE}:latest 
  # и сохраняем в локальный registry
  - docker push ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}
  - docker push ${CI_REGISTRY_IMAGE}:latest

Можем зайти посмотреть, теперь у нас в registry 2 образа.

Так, осталось вылить на прод.

Я пойду по простому пути.

Предположим, что у нас есть 2 площадки: staging и prod. Я хочу, чтобы на стейджинге всегда был последний мастер, а на прод чтобы попадало только избранное.

Самый простой способ реализации "избранного" в гите - это теги. Так и поступим.

Будем копировать все билды из мастера в staging-{hash}, а нумерованные таги в prod-{tag}

Также мы (допустим) обновляем по методу pull через watchtower, а он следит только за одним тегом контейнера (к примеру, nginx:latest или node:12), поэтому будем запоминать последние версии как staging и release соответственно. Как побочный эффект docker pull git.host.com/project:release будет всегда выдавать последний прод.

stages:
- test
- build
- deploy

test:
  stage: test
  image: node:12
  script:
  - npm install
  - npm test

build: # 
  stage: build
  before_script:
  # это предопредленнные перменные, чтобы залогиниться в docker registry
  # из gitlab ci https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
  - docker login -u ${CI_JOB_USERNAME} -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
  script:
  # билдим докерфайл с двумя тегами dev-(номерКоммита) и latest
  - docker build -f ./Dockerfile ./ -t ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA} -t ${CI_REGISTRY_IMAGE}:latest 
  # и сохраняем в локальный registry
  - docker push ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}
  - docker push ${CI_REGISTRY_IMAGE}:latest

deploy-to-staging: # 
  stage: deploy
  before_script:
  - docker login -u ${CI_JOB_USERNAME} -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
  only: # запустить только в ветке мастер 
  - master 
  script:
  # пуллим собранный контейнер
  # не забываем, это может запуститься на совсем другой машине
  - docker pull ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA} 
  # добавляем тег
  - docker tag ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}  ${CI_REGISTRY_IMAGE}:staging-${CI_COMMIT_SHA}
  - docker tag ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}  ${CI_REGISTRY_IMAGE}:staging
  # сохраняем в registry под новыми тегами
  - docker push ${CI_REGISTRY_IMAGE}:staging-${CI_COMMIT_SHA}
  - docker push ${CI_REGISTRY_IMAGE}:staging

deploy-to-prod: 
  stage: deploy
  before_script:
  - docker login -u ${CI_JOB_USERNAME} -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
  only: # запускать только в тегах
  - tags
  # when: # или можно сказать "только вручную"
  # - manual 
  script:
  # пуллим собранный контейнер
  # не забываем, это может запуститься на совсем другой машине
  - docker pull ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA} 
  # добавляем теги
  - docker tag ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}  ${CI_REGISTRY_IMAGE}:prod-${CI_COMMIT_TAG}
  - docker tag ${CI_REGISTRY_IMAGE}:dev-${CI_COMMIT_SHA}  ${CI_REGISTRY_IMAGE}:release
  # сохраняем в registry под новыми тегами
  - docker push ${CI_REGISTRY_IMAGE}:prod-${CI_COMMIT_TAG}
  - docker push ${CI_REGISTRY_IMAGE}:release

Если нужно откатитсья, находим живой билд, и запускаем "deploy-to-prod" руками. он перепишет тег релиз и контейнер оббновится.

Это всё.

Ссылки для общего ознакомления

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment