Русский
Русский
English
Статистика
Реклама

Монорепозиторий

Как мы сетапили монорепозиторий с SSR и SPA для Otus.ru

22.04.2021 16:18:43 | Автор: admin

Привет, хабр! Меня зовут Фёдор и я фронтенд-разработчик в KTS.

В начале 2017 года в KTS обратился давний друг компании Дмитрий Волошин с запросом сделать платформу для онлайн-образования Otus. Сейчас Otus довольно успешный и известный проект, набравший уже десятки тысяч учеников. А тогда он только начинался и состоял всего из одного курса Java разработчика, но планы уже были наполеоновские.

От нас требовалось как можно быстрее собрать портал, на котором можно было бы посмотреть информацию о курсах. В то время современные фронтенд фреймворки и библиотеки были мало распространены, а сделать MVP нужно было как можно быстрее. Поэтому мы решили использовать старую добрую Django с ее мощной админкой. Проект состоял из нескольких страниц, контент которых можно было редактировать внутри админки, а шаблоны джанги, отрендеренные на сервере, отвечали требованиям поисковых оптимизаций.

Но время шло, Otus вырос до гигантских масштабов с кучей внутренних заказчиков: от преподавателей и продюсеров курсов до маркетологов, которым нужна конфигурируемая сквозная аналитика.

Функционально проект состоит из двух модулей: личный кабинет и информационные страницы. В личном кабинете студент и преподаватель могут посмотреть расписание курсов, сдать или проверить домашнее задание, получить консультацию в специальном чате и многое другое.
Информационные страницы - это в основном лендинги курсов и рекламных кампаний, информация о преподавателях и о самом Отусе. Эти страницы важно продвигать в поисковиках.

Проект был реализован на Python + Django, а фронт писался на vanilla js + jquery, которые были распространены в то время. Со временем наша команда добавила на проект множество разных сервисов и технологий: некоторые микросервисы мы писали на Go, на фронт частично внедряли React, когда это уже стало стандартом для всей компании. Но проект разрастался вместе с самим Отусом и в конечном итоге превратился в космический корабль с зоопарком технологий, часть из которых уже потеряла актуальность.

Поэтому мы решили переработать архитектуру и переписать сервис. В этой статье я покажу, как засетапить монорепозиторий на React для разных модулей проекта на примере Otus. Надеюсь, что туториал будет полезен как начинающим, так и уже опытным разработчикам.

Ну что, поехали!

Подготовка

В проекте мы выделили 2 независимых раздела. Информационные страницы должны рендериться на сервере, чтобы обеспечить поисковую индексацию, поэтому предстояло выбрать фреймворк для SSR или сделать собственную сборку. Серверный рендеринг страниц личного кабинета (ЛК) необязателен, поэтому ЛК решили сделать в формате стандартного SPA. При этом важно вынести общие компоненты и логику для переиспользования в двух частях проекта.

Как можно организовать такой проект:

  1. Разделить проект на три репозитория: shared - общий UI-компоненты, сторы с логикой; internal - личный кабинет; external - информационные лендинги. Этот вариант полностью соответствует требованиям, но в данном случае возникает проблема поддержки трех независимых репозиториев, из-за чего может замедлиться разработка.

  2. Хранить все пакеты в одном монорепозитории. Этот вариант чуть сложнее в настройке, но зато на выходе мы получаем один репозиторий и можем быстро обновлять его подпроекты, чтобы эффективнее разрабатывать функционал.

В этом туториале я расскажу про сетап такого монорепозитория. Мы рассмотрим:

  1. Выбор инструмента для управления монорепозиторием

  2. Подключение фреймворка на React, который реализует SSR

  3. Внедрение typescript

  4. Внедрение eslint и алиасов между пакетами

  5. Выбор варианта работы со стилями

  6. Роутинг внутри разделов и между ними

  7. Работу с состоянием приложения

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

Инструмент для управления монорепозиторием

В современном javascript-мире можно выделить два основных инструмента для управления монорепозиторием: lerna, yarn workspaces. Также их можно использовать вместе.

Lerna осуществляет управление с помощью npm или yarn и имеет большое количество утилит для удобной публикации, версионирования и запуска проектов. Yarn workspaces появился позднее, чем lerna, и реализует полный функционал управления несколькими пакетами и их зависимостями.

Мы будем использовать эти инструменты вместе, в этом случае управление зависимостями осуществляется yarn workspaces, и мы получаем удобный интерфейс запуска, публикации, версионирования пакетов от lerna, который понадобится в дальнейшей работе.

Примеры конфигов

lerna.json:

{  "packages": [    "apps/*"  ],  "version": "1.0.0",  "npmClient": "yarn",  "useWorkspaces": true}

Корневой package.json:

{  "name": "otus",  "version": "1.0.0",  "workspaces": [    "apps/*"  ],  "private": true,  "devDependencies": {    "lerna": "^3.22.1"  }}

package.json отдельного пакета:

{  "name": "@otus/external",  "version": "1.0.0",  "private": true}

Начальный сетап internal-пакета

,,

Internal-пакет отвечает за личный кабинет и реализуется в формате SPA.

Для сборки и запуска мы использовали стандартные webpack + babel. Подробно останавливаться на настройке не будем, но если интересно, базовые конфиги приведены ниже.

Примеры конфигов

В webpack.config.js подключаем babel-loader для транспиляции javascript и конфигурируем dev-сервер.

internal/webpack.config.js

module.exports = (opts, args) => {  return {    entry: './src/index.jsx',    output: {      path: buildPath,      filename: `js/[name]-[hash].js`,      publicPath: '/',    },    module: {      rules: [        {          test: /\\.jsx?$/,          exclude: /node_modules/,          loader: babel-loader        },      ],    },    devServer: {      port: 9002,      host: 'localhost',      ...    },  };   ...  };};

Для минимальной конфигурации babel достаточно подключить @babel/preset-env для удобной транспиляции в зависимости от targets и @babel/preset-react для парсинга jsx.

internal/babel.config.js:

module.exports = api => {  api.cache(() => process.env.NODE_ENV);    return {    presets: [      [        require('@babel/preset-env'),        {          targets: {            browsers: ['> 0.25%, not dead']          }        }      ],      require('@babel/preset-react'),    ],  };};

Указываем команду для запуска dev-сервера в package.json

internal/package.json:

{  "scripts": {    "dev": "webpack serve --mode development",   },   ...}

External-пакет. Инструмент для серверного рендеринга

В external-пакете у нас будут информационные лендинги и страницы, которые должны индексироваться поисковиками. Поэтому рендерить контент нужно на сервере. В нашей компании для реализации серверного рендеринга React-приложений мы использовали как готовые решения (Gatsby, Next.js), так и самописную сборку с сервером для рендеринга на Node.js.

Gatsby

Из плюсов можно выделить работу c GraphQL "из коробки". Подходит для Static Site Generation (SSG). Кастомизировать сборку по ощущениям не очень удобно. Как показала практика, Gatsby удобно использовать при разработке небольших проектов, состоящих из статичного контента, например, лендингов.

Next.js

Умеет работать как в SSR, так и в SSG режиме. Поддерживает "из коробки" typescript, css-modules, можно использовать как api-сервер. По ощущениям, кастомизировать сборку проще, чем в Gatsby.

Собственная сборка под SSR с сервером на Node.js

Данное решение очень хорошо зарекомендовало себя в нашей компании, так как не приходится подстраиваться под жесткую архитектуру фреймворка и отсутствует проблема кастомизации настроек: исходя из задачи, мы сами выбираем и подключаем дополнительные инструменты. Кстати, скоро мы собираемся выложить статью про такую сборку.

Если бы мы реализовывали более сложную логику, то скорее всего остановились бы на этом варианте, но в данной ситуации мы решили, что для рендеринга информационных лендингов нам будет достаточно Next.js.

Теперь наш репозиторий выглядит следующим образом:

package.json external-пакета:

{  "scripts": {    "dev": "next dev -p 9001"  }  ...}

Скрипт запуска двух dev-серверов реализуем с помощью lerna:

{  "scripts": {    "dev": "lerna run --parallel dev"  },  ... }

Внедрение typescript

В internal-пакете компиляцию typescript будет выполнять babel. Это позволяет использовать babel-плагины вместе с ts. В Next.js typescript поддерживается "из коробки" и также компилируется с помощью babel.

Для монорепозитория необходимо вынести общие правила в отдельный конфиг tsconfig.base.json. Он должен лежать в корне проекта и называться не tsconfig.json, иначе typescript будет определять корневую директорию как typescript-проект. Каждый пакет должен содержать свой tsconfig.json, который наследует общий конфиг и дополняется специфичными правилами пакета.

Внедрение в internal

Добавляем расширение .ts/.tsx в правило babel-loader:

{  test: /\\.(ts|js)x?$/,  exclude: /node_modules/,  loader: 'babel-loader'}

Добавляем новый пресет в babel.config.js:

module.exports = api => {  ...    return {    presets: [      ...      require('@babel/preset-typescript'),    ],  };};

Конфигурация

Корневой конфиг tsconfig.base.json:{  "compilerOptions": {    "typeRoots": ["./node_modules/@types"],     ...  }}Конфиг в internal:{  "extends": "../../tsconfig.base.json",  "include": ["./src/**/*"],  "exclude": ["node_modules"]}

При создании tsconfig.json внутри external, Next.js автоматически добавит next-env.d.ts файл в корень пакета, и также добавит соответствующую опцию в tsconfig.json.

Конфиг в external:

{  "extends": "../../tsconfig.base.json",  "compilerOptions": {...},  "include": [    "next-env.d.ts",    ...  ],  "exclude": [    "node_modules"  ]}

Подключаем eslint

Подключение eslint похоже на подключение typescript: создаем родительский конфиг, и затем наследуем его в каждом пакете.

При работе в WebStorm важно учесть, что WebStorm запускает процессы eslint во всех пакетах, где eslint указан в зависимостях. Поэтому все пакеты, в которых переопределен конфиг eslint-а должны иметь зависимость на него в package.json, а в корневом package.json этой зависимости быть не должно.

Корневой eslint
module.exports = {  root: true,  parser: '@typescript-eslint/parser',  env: {    browser: true,    es6: true  },  extends: [    'eslint:recommended',    'prettier',    'prettier/react',    'plugin:import/errors',    'plugin:import/warnings',    'plugin:import/typescript',    'plugin:react/recommended',    'plugin:@typescript-eslint/recommended'  ],  parserOptions: {    ecmaFeatures: {      jsx: true    },    ecmaVersion: 2018,    sourceType: 'module',    project: './apps/**/tsconfig.json'  },  plugins: [...],  rules: {...},  settings: { 'import/parsers': {   '@typescript-eslint/parser': ['.ts', '.tsx'] }, 'import/resolver': {   "typescript": {     "project": "tsconfig.json"   }, },}};

.eslintrc.js каждого пакета:

const path = require('path');module.exports = {extends: path.resolve('../../.eslintrc.js'),  ...};

Добавляем удобные импорты

Наша цель - удобно импортировать модули из разных пакетов. Например:

import Button from 'shared/components/Button';

будет импортировать кнопку из общего UIKit-а. Для этого нужно подключить алиасы.

Aliases в internal

Aliases в internal подключаем с помощью webpack. Это гарантирует правильную сборку проекта. Для корректной работы eslint применяется eslint-import-resolver-typescript, в каждом tsconfig.json пакета необходимо указать paths, которые будут использоваться eslint-ом в рамках данного пакета.

webpack.config.js:

module.exports = (opts, args) => {  return {    ...    resolve: {      extensions: ['.ts', '.tsx', '.js', '.jsx'],      alias: {        shared: path.join(appsPath, 'shared/src'),      }    },...  };};

tsconfig.base.json:

{"compilerOptions": {"baseUrl": "apps","paths": {"shared/*": ["shared/src/*"],"internal/*": ["internal/src/*"],"external/*": ["external/src/*"]},    ...}}

Теперь в рамках internal мы можем использовать алиасы.

import * as React from 'react';import { render } from 'react-dom';import Button from 'shared/components/Button'; <-- render(  <div>    <Button />  </div>,  document.getElementById('root'));

Aliases в external

Структура external-пакета:

Для использования aliases в external необходимо прописать paths в tsconfig.json.
При использовании alias на внутреннюю директорию или файл external-пакета проблем не возникает. Но если alias ведет на внешний пакет (в нашем случае на shared), то необходимо воспользоваться next-transpile-modules, с помощью него можно указать, какие еще пакеты Next.js должен транспилировать.
Для подключения необходимо создать конфиг next.config.js.

tsconfig.json:

{  "extends": "../../tsconfig.base.json",  "compilerOptions": {    "baseUrl": "..",    "paths": {      "shared/*": ["shared/src/*"],      "components/*": ["external/components/*"]    },    ...}

next.config.js:

const withPlugins = require("next-compose-plugins");const withTM = require('next-transpile-modules')(['shared']);const plugins = [  [withTM],];module.exports = withPlugins(plugins);

Варианты подключения стилей в проект

Для работы со стилями мы используем scss. Необходимо было выбрать инструмент из следующих:

  1. CSS-Модули (css-modules). В Next.js такой вариант поддерживается из коробки, необходимо просто именовать файлы в формате [name].module.css. Для internal-пакета такая настройка дублируется добавлением лоадера на /\.module\.s?css/

  2. Подход react-css-modules с использованием стилей через атрибут styleName="style" подключается в Next.js "костыльно", да и к тому же пока не поддерживает Postcss 8, и приходится использовать более старые пакеты postcss-nested, postcss-scss.

  3. Использование styled-components. Этот вариант не требует никаких дополнительных настроек сборки в internal-пакете, а для SSR необходимо установить styled-components плагин в babel и прокинуть стили в момент серверного рендеринга в тег head страницы.

Мы решили использовать styled-components. На наш взгляд css-in-js концепция очень удобна, требует минимальной конфигурации и увеличивает скорость разработки.

Для подключения styled-components в internal достаточно установить библиотеку.

При добавление в external необходимо:

  1. Установить библиотеку

  2. Создать кастомный babel.config.js

  3. Подключить babel-плагин для обеспечения совпадения className в момент серверного рендеринга и rehydration.

  4. В next.config.js подключить кастомный babel.config.js с помощью next-plugin-custom-babel-config

Переопределить Document-компонент для вставки сгенерированных style-тегов в тег head в момент рендеринга на сервере.

Код конфигов и компонента

babel-config.js external пакета:

module.exports =function(api) {  api.cache(() => process.env.NODE_ENV);const presets = ['next/babel'];const plugins = [    [      'babel-plugin-styled-components',      {        'ssr':true,        'displayName':true,      }    ]  ];return{    presets,    plugins  };};

next.config.js:

const withPlugins= require('next-compose-plugins');const withTM = require('next-transpile-modules')(['shared']);const withCustomBabelConfig= require('next-plugin-custom-babel-config');const path = require('path');const plugins = [  [     withCustomBabelConfig,    { babelConfigFile: path.resolve('./babel.config.js') },  ],   [withTM],];module.exports = withPlugins(plugins);

_document.tsx:

import Document,{  Head,  Main,  NextScript,DocumentContext,DocumentProps,Html,} from 'next/document';import * as React from 'react';import { ServerStyleSheet } from 'styled-components';class MyDocument extends Document<DocumentProps & { styleTags:Array<React.ReactElement> }> {static async getInitialProps(ctx: DocumentContext) {const initialProps = await Document.getInitialProps(ctx);const sheet = new ServerStyleSheet();const page = ctx.renderPage((App) => (props) =>      sheet.collectStyles(<App {...props} />)    );const styleTags = sheet.getStyleElement();return { ...initialProps, ...page, styleTags };  }  render() {   return(      <Html>        <Head>{this.props.styleTags}</Head>        <body>          <Main />          <NextScript />        </body>      </Html>    );  }}export default MyDocument;

Итоги

В итоге мы сделали функционирующий сетап:

  • подключили lerna + yarn workspaces для управления нашим монорепозиторием;

  • внедрили и, исходя из наших задач, кастомизировали Next.js;

  • подключили typescript;

  • наладили работу импортов вместе с eslint;

  • внедрили styled-components

Если данный материал вам показался интересным, в следующей статье я расскажу о том, как мы добавляли роутинг внутри каждого модуля и между модулями, подключали стейт-менеджер и опишу схему работы приложения в проде.

Весь код можно посмотреть здесь.

Подробнее..

Монорепо жизнь до и после

15.12.2020 10:11:51 | Автор: admin
Это история о том, почему в одном из направлений Юлы отказались от практики отдельных репозиториев на микросервисы и внутренние библиотеки, перейдя на монорепозиторий, и что из этого вышло. О проблемах, с которыми столкнулись в компании, и тех, которые получилось решить при помощи этого переезда, рассказал на конференции Golang Live 2020 руководитель b2b-разработки Юлы Валентин Дубровский.



В этой статье мы поговорим о:

  1. Проблемах, которые решает монорепо;
  2. Минусах монорепозитория;
  3. GO в команде B2B Юлы;
  4. Том, как мы вводили монорепозиторий.

Внимание! Я буду говорить не про монолит. У многих любое моно- ассоциируется именно с ним.



Речь пойдет о монорепозитории.

Какие проблемы решает монорепо?


Десять лет назад фронтенд и PHP-код хранились у нас в одном репозитории по 10 Gb (GitLab/GitHub).

Когда пришла микросервисная эра, расклад изменился: один микросервис один репозиторий. Это логично, потому что одна команда следит за несколькими микросервисами и контрибьютит только в них, не ломает ничьи процессы, не встраивается в код-ревью других команд. Наверное, каждый, кто начинал с микросервисов, делал их в отдельных репозиториях.

Сегодня мы попробуем осмыслить шаг назад и поговорим про монорепозиторий.

Монорепозиторий это когда все микросервисы хранятся в одном репозитории, вместо отдельных на каждый сервис. Мы берем несколько репозиториев, в каждом из которых находится его сервис, или библиотека, и переносим их в один.

Для чего это нужно? Почему бы и дальше не жить по стандартам один сервис один репозиторий, или одна внутренняя библиотека один репозиторий? С какими проблемами нам поможет справиться монорепо?

Ориентирование на местности


Начнем с истории. Разработчик получает задачу: ему нужно создать новый сервис. Первым делом он создает репозиторий, настраивает CI и далее совершает некий порядок действий. В зависимости от компании и процессов, этот порядок будет различаться.

А потом приходит его коллега и спрашивает: Ты делал сервис, который загружает картинку пользователя. Расскажешь, где его найти?. Он не может сам сориентироваться по поиску через GitLab/GitHub в устаревшей документации сервиса. Поэтому обращается к разработчику сервиса напрямую.

Есть проверенная на практике теория о том, что монорепо может улучшить ориентирование, потому что вы скачиваете только один репозиторий и, после этого, видите всю картину целиком. Например, наблюдаете за появлением новых обновлений.

Когда приходят новенькие, вы проводите им экскурсию по всем сервисам. Но это проще сделать в условном монорепо. Новеньким обычно сложно понять всю структуру проекта через десяток репозиториев.

Приватные репозитории


Кроме того, монорепозиторий позволяет решить проблему работы с приватными репозиториями.

Рассмотрим сценарий. В базе данных есть адаптер: Redis, MongoDB обертка над общей публичной библиотекой. Мы делаем для нее отдельный репозиторий. И осуществляем некий порядок действий: добавляем CI, который прогоняет тесты, навешиваем тэги при релизе master. Дальше начинаются танцы с бубнами. Как притянуть эту библиотеку в наш репозиторий? Ведь через Go.mod это не так просто сделать.

Если хочется, например, обновить библиотеку, чтобы она обновилась во всех сервисах, нужно подтянуть новую версию в каждом репозитории. Это лишние телодвижения, которых можно избежать. И монорепо позволяет это сделать.

Одна задача один Pull request


Еще один интересный плюс монорепозитория связан с самим выполнением задачи.

Разработчик берет задачу, которая затрагивает несколько сервисов. Например необходимо что-то сделать в GraphQL, или вокруг микросервисов.

У него два алгоритма.

Первый. Сделать все разом: править три микросервиса, чтобы получить изменения в одном. Их нужно выставить на ревью по одному. В зависимости от процесса, это будут ревьювить разные разработчики, у которых нет единого контекста.

Второй вариант решения этой проблемы декомпозиция задачи по сервисам. Там есть общая родительская задача. Ревью проходят по одному, вне контекста общей задачи. Или же можно попробовать сымитировать контекст. Но это все дополнительные действия, которые нужно предпринять, несмотря на то, что задача маленькая. Ведь изменилось условно 15-20-30 строчек кода.

Если задача небольшая, монорепо позволяет сделать все в рамках одного pull request. Ревью сможет заняться один разработчик, у которого будет полный контекст по задаче. Это удобно и ускоряет поставку задач на production из-за того, что мы избавляемся от лишнего оверхеда.

Приведу немного надуманный пример, который, тем не менее, позволяет понять проблему глубже.

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

Улучшаем командный процесс


Эта проблема позволяет понять, насколько в монорепо проще организовать процесс кроссревью и шаринг знаний. В нем вы будете видеть, что делают другие команды, или хотя бы замечать появляющиеся обновления, а не просто искать их по GitLab.

Эти вопросы можно решить и в полирепозиториях. Но в монорепо это происходит само по себе.

Мне есть что сказать и про переиспользуемый код. Например, вы используете один и тот же адаптер допустим для Redis в двух сервисах. Не будет ли вам лень переносить это в отдельный репозиторий, как внешнюю библиотеку? Я нередко сталкивался с тем, что когда есть общий дублированный код, в разные сервисы его не выносят, потому что это долго и муторно.

В монорепо это решается внесением провайдера в отдельную директорию и использованием его во всех сервисах. Это не требует дополнительного оверхеда.

Окружение


Наверное, каждый мечтает о том, чтобы было возможно локально поднимать все окружение по щелчку пальцев. Монорепо позволяет решить эту проблему, причем достаточно просто.

Есть инструмент docker-compose, который просто идеален для локальной разработки. В нем настроен билд сервиса и запуск. И это прекрасный вариант, не требующий лишних действий.

Базовые штуки в локальном окружении настроить очень просто. Например, запуск линтера на pre-commit hook. Мы должны закинуть в .git скрипт, который будет запускаться на pre-commit hook. Это позволяет отлаживать простой код на локальном уровне, а не в CI.

Минусы монорепозитория


В любом процессе, технологии, решении есть свои сложности. Самый большой минус монорепозиториев это проблема зависимостей.

Когда у нас есть отдельный репозиторий на каждый сервис, мы можем там использовать совершенно разные мажорные версии библиотек. Например, в одном репозитории драйвер для Mongo шестой версии, а в другом этот же драйвер пятой версии. И с этим не возникнет никаких проблем.

В монорепозитории так не получится, и придется следить за breaking changes. Мы не можем использовать разные мажорные версии в разных репозиториях. Кроме того, нужно внимательно следить за внутренними библиотеками.

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

При использовании монорепа усложняется CI/CD процесс.

Если мы живем в парадигме один сервис один репозиторий, мы находимся в 4 джобах: test, build, deploy и что-нибудь еще. Слили master, сбилдили, задеплоили ОК.

В монорепозитории это сложнее, потому что при сливе master, нам нужно билдить только то, что изменилось. Монорепозиторий будет потихоньку разрастаться. В нем может быть 30, 40, 50, 100 сервисов. И если мы будем билдить все, ни к чему хорошему это не приведет.

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

GO в команде B2B Юлы




У нас несколько десятков микросервисов на GO.

GRPC и Kafka используются для общения между микросервисами, GRPC для синхронных, Kafka для асинхронных. Это важно, потому что GRPC по протофайлу представляет нам генерированный клиент, и в монорепозитории его достаточно просто использовать между микросервисами. Для клиентов мобильных приложений и веба мы используем GraphQL.

Gitlab CI используется для билда Docker-образов и для выливки на тестовые стенды.Мы используем Gitlab CI для всех наших CI-процессов, пишем его сами без DevOps. DevOps-инженеры помогают дать нам пайплайн, который мы дернем для того, чтобы вылить сервис на тестовое окружение.

Наши сервисы имеют однотипную структуру, которую мы определили на раннем этапе.
Это не стандартный Golang package layout, а своя версия.

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

В каждом сервисе должен быть реализован CI/CD. Базовые штуки мы вносили в Gitlab CI шаблоны, и инклюдили. Если вы знакомы с Gitlab CI, понимаете, о чем я назовем это переиспользованием шаблонов.

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

Я уже упоминал, что с приватными репозиториями не все так просто, поэтому мы сделали небольшой ход конем: подняли Athens. В ней можно указать путь до GitLab и креды до него, это позволяет стягивать приватные репозитории через GOPROXY. Для каждого разработчика, для каждой CI машины это выглядит так: ты делаешь пустой приватный пакет, указываешь Go proxy до Athens, и она упрощает жизнь.

Кроме того, мы сделали отдельный репозиторий для docker-compose, где указывали latest до registry каждого сервиса.

Как мы вводили монорепо


Сначала нужно было объединить репозитории.

Это сложный процесс, и мы разбили его на шаги:

  • Все версии внешних библиотек свести к одному знаменателю. Это тяжелый процесс, потому что он требует дополнительного тестирования;
  • Выбрать стратегию того, как это делать.

Первая стратегия выглядит следующим образом: мы все фризим. Для этого подготавливаем инфраструктуру в монорепо. Это самый простой паттерн действия.

Но мы пошли по другому пути. Выделили разработчика, который по одному репозиторию, начиная с самых редко используемых, фризил ветку master. Потом переносил новый сервис, запускал его в монорепо и передавал о нем информацию в чатик.

Проблемы возникли на последних сервисах, которые активно используются. Например, GraphQL Getaway. В этот момент пришлось подключать командные действия. На этом этапе перенести все в монорепозиторий получилось меньше, чем за сутки.

Дальше мы переходим к локальному окружению.

Как я уже говорил, docker-compose идеально для этого подходит. Лучшая стратегия: отправить через volume в каждый билд путь до до локальной директории go modules. Тогда, если вы что-то делаете go.mod, оно попадает в GOPATH/pkg/mod. И это ускорит билд.

И конечно же добавить какие-нибудь вкусняшки. В нашем случае это был UI для GRPC. Мы на каждый сервис сделали свой UI, вывели порт наружу. То есть можно было просто собирать GRPC-запросы в формочки.

После чего мы добавили rest proxy для Kafka. Он позволяет продьюсить и консьюмить из Kafka через обычный Postman.

Теперь поговорим о билдах.

Мы пошли по такому сценарию: для каждого сервиса использовали директиву Gitlab CI only changes. Записали в only changes Dockerfile, go.mod, наши внутренние библиотеки и все зависимости от внешних сервисов:



Это все здорово работает до тех пор, пока не меняется что-то общее, например, go.mod. Когда у нас сливается сервис, в который добавилась новая библиотека, CI начинает все билдить.

В этом случае помогает автоматизация. Например, с помощью Go-кода мы можем вытянуть все зависимости нашего приложения, и билдить, только если они изменились.

Что мы сделали еще? Раньше у нас были пухлые Gitlab CI файлы. Но в монорепо можно обращаться к локальным скриптам. И мы выделили папку script на CI и тяжелые вещи, которые нужны в CI, пишем на Go-коде. Например, выбор ревьювера.

Выбор ревьювера эволюционировал у нас в интересном формате. Сначала мы просто скидывали в чат созданные pull requests. Но ревью проходили долго потому, что все надеялись друг на друга и не приступали к задаче.

На втором этапе нашего развития, в чат скидывался Slack, в нем PR, и автоматически рандомно из чата выбирался ответственный за ревью.

С монорепо мы решили перенести это в CI. Когда аттачится pull request, мы запускаем наш Go-скрипт и выбираем двух случайных ревьюверов. Первого на основе измененных файлов: кто больше всего трогал, тот и выбирается рандомно среди топ-кандидатов. Второго из всего проекта монорепо.



Это очень круто работает.

Естественно, в монорепозитории как по маслу пошли такие вещи, как нотификация при открытии PR, или нотификация, если PR долго не ревьювятся. Каждые 2-3 часа мы напоминаем: Эй, не забудь про ревью!.



Это повышает конверсию из pull requests в релиз, работает коллективная ответственность.

У нас нет Continuous Delivery в production, а релиз происходит так:



Сейчас в нашей компании почти continuous delivery в production (кстати, он есть на тестовые стенды), но еще не совсем. Думаю, мы дойдем на него на следующей стадии.

Выводы


У монорепо множество плюсов, которые помогают существенно облегчить работу. Он позволяет решить проблемы с приватными репозиториями, улучшить ориентирование, монорепо просто идеален для локальной разработки. А еще в монорепозитории намного проще организовать процесс кроссревью и шаринг знаний.

Для нас переход в монорепозиторий стал успешной практикой, поскольку:

  • Ускорилось ревью.
  • Задачи стали поставляться быстрее и с меньшим количеством блокеров.
  • Получилось добавить много интересных плюшек в рамках одного репозитория за счет того, что в монорепозитории мы делаем общий шаблон.


Возможно, в очень большой компании стоит делать монорепозитории по направлениям.

У нас в Юле есть несколько отделов. Направление монетизации, направление продукта и направление платформы. На данном этапе мне кажется, что переводить всех в один монорепо не стоит. Но можно попробовать создать монорепозиторий для каждого из них.

Хотите больше материалов по go-разработке? У вас есть возможность купить видео с Golang Live 2020.

В этом году на конференции сделали акцент на Go в продуктовой разработке и раскрыли эту тему с трех сторон: как справиться с проблемами Go и какие есть тулзы, почему все-таки стоит выбрать именно Go, и как после этого поддерживать продукт.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru