Setting up a CI/CD pipeline in GitLab: from monolith to modules

A monolithic pipeline slows down releases: the build pulls the entire project along, and a small change turns into manual work. We set up GitLab CI/CD turnkey — we move the build to modules, remove manual steps, and noticeably reduce release time. We will discuss your task and come back with a migration plan.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1359
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Setting up GitLab CI/CD: why a monolithic pipeline breaks

While a project has one service and one repository, a single build pipeline script looks reasonable: one configuration file at the root, a run on every commit, and inside — tests, artifact builds, and deployment through a chain of stages.

The problem is that it grows along with the product. Gradually it accumulates dozens of stages, branching by rules, dependencies between steps rely on declaration order, and part of the deployment is performed manually from the UI.

Hence the long release. A change in one module triggers a full run: all components are rebuilt, dependencies are pulled again, and checks unrelated to the change are executed again.

The cache does not help because the key is shared across the entire script and is reset on the first edit. Feedback wait time grows, engineers start batching commits, and the cost of an error in each run increases.

Fragility manifests differently: people stop changing the script because they are afraid. Editing one stage breaks a neighboring one, a step with allow_failure masks a failure, and the error surfaces already in production.

Manual deployment steps are added as a temporary solution, accumulate verbal agreements, and ultimately depend on one person: they know the sequence of actions, not the pipeline.

The bottleneck in a build is not runner capacity but coupling. While the build logic is the same for all components, any change has to be verified in full, and responsibility cannot be divided between teams.

Below we look at how this is addressed: extracted configuration modules, explicit contracts between stages, and predictable rollback points.

What modular builds provide: release speed and a class of failures

A modular build in GitLab CI/CD starts with moving recurring steps — dependency installation, test runs, artifact build, image publication — into shared definitions that are included in each project.

A specific build describes only what differs: the set of checks, the branch, the environment. Jobs that are identical in meaning run in parallel instead of queuing up, and build time drops not by cutting checks but because they stop waiting for each other.

Recovery time is determined by how narrowly the failure is localized. While steps live in one long definition, any edit means restarting the entire script from scratch.

In a modular scheme, a specific block fails, and only it is restarted, with the dependency cache preserved: recovery is measured in minutes, not by rerunning the whole chain.

The share of manual operations is reduced to isolated approval points.

Artifact build, migration application, and environment rollout are described declaratively and run the same way in staging and in production — the engineer does not assemble the release by hand or copy files to the server.

An entire class of failures disappears: configuration drift between environments, forgotten steps, edits directly on the server, and "works locally, not in staging." Identical modules mean that the verified path does not change during rollout.

A side effect of step reuse is that a new service connects to the ready-made scheme in minutes instead of accumulating its own set of commands that no one maintains later.

Build organization options: monolith, modules, shared pipeline

A monolithic build — one configuration file for all services — stops scaling when more than one team works on the repository. Editing a shared block affects other teams' stages, and jobs copied between projects diverge after just a few releases. Below are four alternatives and the boundaries beyond which each starts to get in the way.

In GitLab, all options are assembled from the same set of primitives: templates are included with include directives, reusable jobs are described as separate blocks with parameters, and isolated environments are defined by an image and a set of variables. The difference is not in syntax, but in where the responsibility boundary between teams lies.

Option When it fits Limitations
Monolithic build One service, one team, infrequent releases Editing a job affects other stages; jobs are duplicated across projects
Modular templates Multiple teams, shared build and verification stages Templates must be versioned; otherwise projects diverge in behavior
Shared step libraries Many similar services, frequent changes to verification rules Changing a step affects all consumers — the library is tested separately
Isolated environments Different stacks, strict reproducibility requirements More resources; dependency cache and artifact passing are mandatory

In practice, a combination is used: a shared step library plus modular templates at the team level, while the monolith remains for auxiliary checks. Reusable jobs are not rewritten in each project, and environment isolation does not break neighboring builds.

How to migrate a build to modules: engineering work stages

Migrating a build from a monolith to modules is not editing a single configuration file, but a sequence of work where each step is verified on a live release.

The order is critical: if you start with environment setup without understanding the connections between modules, the pipeline will start failing where it previously passed as a whole.

  1. Analysis of the current state. We measure full build time, find recurring steps, non-working cache, and transitive dependencies. The output is a map of bottlenecks with numbers, not feelings.

  2. Designing the build scheme. We define module boundaries, rules for triggering by changed paths (rules:changes in GitLab), and what is reused: shared templates and included configuration fragments. We separately record which steps run in parallel and which must remain sequential.

  3. Environment setup. We move secrets and variables into protected sets, separate environments, and align build runners — the result must not depend on which machine the pipeline started on. Cost is calculated after an audit. Verification on real releases.

    We run the scheme across several releases: we measure build time, the share of cached steps, and behavior when one module fails. An error in a module no longer blocks the others.

  4. Handover and training. We provide documentation on the pipeline structure, change rules, and a walkthrough with the team — so that changes are made according to the rules, not by random edits.

What remains in the repository and documentation after handover

Handover is not "we set it up and left," but a set of files your team uses to make changes on its own, without messaging us.

Everything is in the repository next to the code and lives in the same change history, so any build edit goes through review like a regular commit.

  • Build configuration: the root file in GitLab and included modules by stages — preparation, tests, publication. Each block has a comment: why it exists and what will break if it is removed.
  • Templates of typical jobs: a new similar service is connected by specifying a path and variables, not by copying hundreds of lines.
  • Environment scheme: which branches are published to the test and production environments, where secrets are stored, and how they differ from regular variables.
  • Release policy: who initiates a release, which checks are mandatory, how to roll back to a previous version, and when a release is blocked automatically.
  • Team instructions: how to add a service to the build, how to reproduce a failure locally, and how to read the log of a failed job.
  • Retention policies: lifetimes of artifacts, intermediate build data, and temporary environments — so that storage volume does not grow on its own.
  • Access matrix: project roles, who may change the configuration, protected branches, and who can trigger a release.

This set is enough to manage the build on your own from day one: typical changes are made according to the instructions, not through us.

Case study: how releases were accelerated and manual build steps removed

Releases in the project went through one common build pipeline: a change in any module triggered a rebuild of the entire product, and deployment to staging was performed with a set of manual commands — some steps were kept in a specific engineer's head.

A release took 40–60 minutes, and roughly every fourth change was rebuilt again due to unstable environments.

What was changed

The build was moved to GitLab and split by modules: each service has its own set of stages, dependency cache between runs, and an environment matrix where test and production environments are described by the same code.

Manual operations became declarative steps — migrations, cache warming, traffic switching on staging. The artifact is built once and promoted across environments without rebuilding, so exactly the same build that passed the checks reaches production.

Metrics before and after

Metric Before After
Release time 40–60 min 9–12 min
Manual deployment steps 14 2
Share of repeated builds 24% 4%
Failures due to unstable deployment 3–4 per quarter isolated

What was eliminated by failure class: drift between staging and production, "forgotten" migrations, and rebuilds after a failed run. Rollback is now rerunning the previous job, not restoring by instruction: return to a working state was reduced from 30 minutes to 2–3.

What to do if the pipeline in GitLab is already deployed

Breaking a working scheme is unnecessary and unprofitable: while it builds artifacts, the team has a time buffer for a calm replacement.

The right path is not a rewrite from scratch, but a gradual migration in which the new configuration coexists with the old one and can be disabled at any moment with one revision.

Technically, this is a parallel run: the new pipeline is described by a separate configuration file in the same repository, and GitLab includes it by rules — for some branches, tags, or by the value of a trigger variable.

Setting up GitLab CI/CD in this mode comes down to the existing build scheme continuing to serve release branches until the new one proves its results on a real stream of changes.

The key condition is backward compatibility at the boundaries: stage names, cache, runners, and artifact contracts are preserved so that deployment jobs and external consumers do not notice the swap.

Migration proceeds one block at a time — first linters and tests, then image build, then deploy — and each step is checked separately.

Rolling back changes does not require manual rebuilding: the configuration is versioned together with the code, so rollback is reverting one commit, not restoring the scheme from memory.

When the new scheme over a series of runs gives no worse execution time and the same share of successful builds, we disable the old one and remove its description as a separate step.

A separate benefit is modularity: extracted blocks are reused across repositories via included template files. Editing one verification rule is done in one place, not in ten configurations, and new projects start on a ready-made scheme.

Common technical concerns before migrating to modules

Concerns before migrating a GitLab CI/CD configuration to modules are almost never about syntax — they are about operations: where secrets are stored, who is allowed to edit the shared template, what happens if one service lags behind the others, and what the team will be left with after handover.

Where to store secrets if the configuration has become shared?

Keys and tokens do not go into the repository: they are stored in group variables with masking and are bound to protected branches and environments. The template accesses them by name; values remain in the settings — during rotation, one entry is changed, not every project.

How to separate access so the shared template is not accidentally broken?

We move modules into a separate repository: only a narrow circle of engineers can write to it; others include it as a dependency. A developer builds their service but does not edit shared rules and protected branch variables — template changes go through review like regular code.

What happens if modules start diverging by version?

We pin dependencies: a project references a specific template version, not its current state. Then updating one service does not drag along other builds, and rollback is changing the link to the previous version. Once per agreed cycle, we update the modules in a batch.

What will remain for the team after handover?

We hand over not only files but also working rules: where to change variables, how to add a new module, what to do when a shared job fails. Plus checks that catch configuration drift before merge, and documentation for each block.

Let's discuss the task: we'll analyze your pipeline and come back with a plan

The scope estimate is based on facts, not general words: the current pipeline shows how many stages will have to be broken down into modules and where manual steps are hidden.

Access to production and stopping releases are not needed for this — configuration and process descriptions are enough.

  • Build configuration file from the repository — as is, with all included templates and inheritances.
  • List of repositories and services it builds, noting which are linked by shared stages.
  • Timeline of one typical build: how long stages take, what is done manually, where it most often fails.
  • List of environments and deployment rules — who and under what conditions releases the build to each of them.
  • Requirements for secrets and permissions: which variables are hardcoded directly in the configuration, where keys are stored, what cannot be exposed.
  • External systems connected to the pipeline: image registry, artifact storage, monitoring, and notifications.
  • Constraints: release windows, allowed downtime, security policies, and audit requirements.

Describe the task in two or three paragraphs and attach what you already have — even a partial set gives an understanding of the scope.

Based on this data, we will come back with a migration plan: stages, timeline for each, team composition, and points where your engineer will be needed.