Skip to content

Build Amazing Pipelines: GitLab CI

  • 18 min read

Tired of deployment day drama? Do your builds fail more often than they succeed? You’re not alone. Many teams struggle to create a smooth and reliable software delivery process. It’s often complex and difficult to manage. But what if I told you there’s a way to build amazing pipelines that streamline your work?

With GitLab CI, you can automate and speed up your entire software development lifecycle. From code commits to final deployments, you’ll find a way to make the process easy. It’s a powerful tool that helps you create custom workflows that fit your needs. It’s easy to set up and easy to use. This guide will help you to understand how it works. We’ll walk you through each step to create effective build pipelines with GitLab CI.

What is GitLab CI?

GitLab CI is a continuous integration (CI) tool that is part of GitLab. It helps you automate the testing, building, and deployment of your code. You define your pipeline in a YAML file named .gitlab-ci.yml. This file lives in your repository. It’s your way to tell GitLab how to build your project.

GitLab CI has become a favorite among developers. The 2023 GitLab survey showed that 70% of developers use CI/CD practices. GitLab CI has a simple way to set up. You don’t need to set up a separate server. You can run the jobs directly from GitLab’s interface. You can also easily scale your build process. There is support for Docker and other technologies. This tool becomes a true ally when building modern software.

Why Use GitLab CI?

You might ask, “Why should I care about GitLab CI?” Well, let’s check out some of its big benefits:

  • Automation: Say goodbye to manual tasks. GitLab CI automates the whole process. It makes sure each change is tested. Each change is also built with consistency. You reduce the chances of human error.
  • Faster Feedback: The system runs tests fast. It gives you quick feedback on every code change. This helps you find and fix issues early. It also makes the development process faster.
  • Better Quality: With automated testing, you’re more likely to catch bugs before your code goes live. This leads to high-quality software.
  • Consistency: GitLab CI makes sure that your builds are always the same. No matter who pushes the code. This means that what you test is what you deploy.
  • Flexibility: You get a chance to create complex workflows. It fits different needs. Whether you have simple web apps. Or complex microservices, GitLab CI can handle it.
  • Easy Integration: Because GitLab CI is part of GitLab, it works well with other features. This gives you smooth workflows. You’ll see it goes well with code management and project tracking.
  • Cost-Effective: You don’t need extra tools or servers. GitLab CI is already in the system. This makes it cost-effective, especially for small teams.
  • Improved Collaboration: It’s easy to see the status of each build. This makes it easier for team members to work together.

GitLab CI Core Components

To use GitLab CI effectively, you must understand its main parts. Here are a few important concepts:

  • Pipelines: These are the top-level components. A pipeline is a set of tasks that define the software delivery process. A pipeline has stages, jobs, and other parts.
  • Stages: These are a set of jobs. They’re run in sequence. For example, you might have a “build” stage, a “test” stage, and a “deploy” stage.
  • Jobs: These are the specific tasks that run within stages. Jobs do things like compile code, run tests, or deploy software.
  • Runners: These are the agents that execute the jobs. You can use shared runners provided by GitLab. Or you can set up your own. Runners are essential. They do all the heavy lifting.
  • .gitlab-ci.yml File: This is the config file. You use it to define your pipelines, stages, and jobs. It’s always in the root directory of your project. This file is the heart of your CI/CD setup.

Getting Started with GitLab CI

Now that you know the basics, let’s set up GitLab CI for your project. Here’s how you can get started step by step:

1. Create a GitLab Project

First, you need a project in GitLab. If you have one already, you can skip this step. To create a new project:

  1. Log in to your GitLab account.
  2. Click the “Create new project” button.
  3. Select the type of project:
    • Create blank project” if you start a new project
    • Import project” to import an existing one from another platform like GitHub.
    • Create from template” if you want to use a pre-made project structure.
  4. Enter the project name, URL, and set the visibility. You can make it public or private.
  5. Click “Create project.”
  6. Go to your new project page.

2. Add the .gitlab-ci.yml File

The next step is to add the configuration file. This is where you define how your build process works. To add the .gitlab-ci.yml file:

  1. Go to your project page.
  2. Click “+” then “New file”.
  3. Name the file .gitlab-ci.yml.
  4. Add the base config to the file. We’ll show you how to write it in the next step.
  5. Click “Commit changes”.

3. Configure Your First Pipeline

Now, let’s write the base code for your first pipeline. Here is an example of what your .gitlab-ci.yml file might look like:

stages:
  - build
  - test
  - deploy

build_job:
  stage: build
  script:
    - echo "Building the project..."
    - echo "Build complete."

test_job:
  stage: test
  script:
    - echo "Running tests..."
    - echo "Tests passed."

deploy_job:
  stage: deploy
  script:
    - echo "Deploying application..."
    - echo "Deployment done."

Let’s break down the code above.

  • stages: This part defines the names of the pipeline stages. In this case, we have build, test, and deploy.
  • build_job, test_job, deploy_job: These are three jobs in the pipeline. Each has a stage, which is when the job should run, and a script, which is the set of commands to run.

4. Run Your Pipeline

After saving the .gitlab-ci.yml file, GitLab will automatically detect it. And it will start the pipeline on the first commit. To view the pipeline:

  1. Go to your project page.
  2. Click “CI/CD” then “Pipelines”.
  3. You will see your pipeline running. Click on it to see more info.
  4. You can view each job, its logs, and the overall pipeline status.

You can make sure that all stages run in order. First, the build_job, then the test_job, and then deploy_job. If any job fails, the pipeline will stop. This way you catch errors early.

Understanding the .gitlab-ci.yml File

The .gitlab-ci.yml file is where you define all your CI/CD magic. Let’s check out the structure and common elements:

Core Structure

The file consists of a few key parts:

  • stages: This defines the order in which jobs will run.
  • jobs: Each job has its own definition, like:
    • stage: The stage the job belongs to.
    • script: The commands to run for the job.
    • image: The Docker image to use for the job.
    • services: Docker services to use with the job.
    • variables: Environment variables for the job.
    • before_script/after_script: Commands to run before and after the script.
    • only/except: Define when the job should run (based on branches, tags, etc.)
    • artifacts: Files to save after the job.
    • cache: Files and directories to cache between jobs.

Common Keywords

Here are some common keywords you’ll use in your .gitlab-ci.yml file:

  • image: This specifies the Docker image to run the job in. For example, image: node:latest uses the latest Node.js image.
  • script: A list of shell commands to execute. These commands can be anything from compiling code to running tests.
  • before_script: Commands to run before the main script commands. Useful for setting up the environment.
  • after_script: Commands that run after the script. Use it for cleanup.
  • variables: Allows you to define environment variables for the job. You can use them in your scripts.
  • artifacts: It tells GitLab which files to keep when the job finishes. Useful for passing files between jobs.
  • cache: Specifies files that should be saved between different runs of the pipeline. It makes the pipeline faster.
  • only: A list of conditions for when the job will run. Like branches or tags.
  • except: Similar to only, but it defines when not to run a job.
  • services: This keyword allows you to run specific Docker services. For example, you can use a database service.
  • tags: Specifies which runner should run the job. Runners must have matching tags.

Examples of Complex Configurations

Here’s an example of a more complex configuration with more keywords.

image: node:latest

stages:
  - build
  - test
  - deploy

cache:
  paths:
    - node_modules/

build_job:
  stage: build
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/

test_job:
  stage: test
  script:
    - npm test
  dependencies:
      - build_job
  only:
    - main

deploy_job:
  stage: deploy
  script:
      - echo "Deploying..."
      - echo "Deployed successfully"
  only:
    - main
  environment:
    name: production

In this example:

  • We define the base image as node:latest.
  • The cache setting saves the node_modules directory.
  • The build_job installs dependencies, builds the project, and saves the dist/ directory.
  • The test_job runs tests and depends on build_job artifacts.
  • The deploy_job uses a production environment and runs only on the main branch.
  • The test_job depends on the artifacts of build_job

These options will let you define complex tasks for your build process.

Advanced GitLab CI Techniques

Now that you know the basics, let’s dive into advanced techniques to make your pipelines even more effective:

1. Using Docker Images

Docker images let you isolate and control the environment for your jobs. This means you can reproduce the build process without worries. The image keyword defines the Docker image to use.

build_job:
  stage: build
  image: maven:3.8-jdk-17
  script:
    - mvn clean install

In this example, the build job will use a Docker image that has Maven. You can also use your own private images.

2. Caching Dependencies

Caching saves time and resources. It makes build times faster by storing data that is used in multiple jobs. The cache keyword sets the files to save.

cache:
  paths:
    - node_modules/

build_job:
  stage: build
  script:
    - npm install

This example caches the node modules. The next time the pipeline runs, dependencies will load from the cache.

3. Environment Variables

Environment variables let you configure your builds without exposing sensitive info in your .gitlab-ci.yml file. Use them for passwords, tokens, and API keys. You can define them on the settings page of your project. You can also set variables within the .gitlab-ci.yml file.

variables:
  API_KEY: "your-api-key"
deploy_job:
  stage: deploy
  script:
    - echo "Deploy with API key: $API_KEY"

This shows how you can set environment variables for a single job.

4. Pipeline Triggers

GitLab CI lets you trigger pipelines manually. Or it can trigger them automatically based on many events, such as new commits, branch merges, and schedules. This gives you a wide range of control on how your build pipelines run.

Manual Triggers:

To trigger a pipeline manually:

  1. Go to your project.
  2. Click “CI/CD” then “Pipelines”.
  3. Click “Run Pipeline” and choose the branch or tag, then “Run Pipeline”.

Scheduled Triggers:

Set up scheduled triggers:

  1. Go to “CI/CD” then “Schedules”.
  2. Click “New schedule” and fill in details to run the pipeline on a time pattern.

Webhook Triggers:

Use Webhooks for external events. Configure them in Settings > Webhooks.

5. Parallel Jobs

You can run jobs at the same time. This speeds up your pipeline. To run jobs in parallel, define them in the same stage.

stages:
  - test

unit_test:
  stage: test
  script:
    - echo "Running unit tests..."

integration_test:
  stage: test
  script:
    - echo "Running integration tests..."

In this case, both the unit_test and integration_test jobs run in parallel at the test stage.

6. Using Artifacts

Artifacts pass files from one job to another. Use them for build outputs, test results, or config files.

build_job:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/

deploy_job:
  stage: deploy
  dependencies:
    - build_job
  script:
    - echo "Deploying dist/ directory..."

The build_job saves the dist/ directory. Then, the deploy_job uses it.

7. Conditional Jobs

You can define conditional jobs using only and except keywords. These make jobs run only in specific cases.

deploy_job:
  stage: deploy
  script:
    - echo "Deploying..."
  only:
    - main

This job will only run when the commit is on the main branch.

8. Secure Secrets Management

Avoid putting secrets in the .gitlab-ci.yml file directly. Instead, use secure ways to store these:

  • GitLab CI/CD Variables: Store secrets as CI/CD variables in your project settings. Use them in jobs with environment variables.
  • Vault: You can use HashiCorp Vault as an external secret manager. This is a safe way to handle secrets.
  • AWS Secrets Manager: For AWS users, store the secrets in AWS Secrets Manager. GitLab CI can get these values using AWS integration.
  • Google Secret Manager: For GCP users, similar to AWS you can use Google Secret Manager to safely store your secrets.
  • Other secret managers: You can use other secret manager solutions, but this will require setting them up for GitLab CI to use them.

Remember that you must handle sensitive data with care to prevent security threats.

9. Custom Runners

GitLab CI Runners are agents that execute jobs. By default, GitLab provides shared runners. But you can set up your own runners. This gives you more control of the setup.

Setting up a Custom Runner:

  1. Go to your project settings.
  2. Go to “CI/CD” then “Runners”.
  3. Follow the instructions to register a runner on a machine you control.
  4. You can also run your runner on docker.
  5. After you set up a runner, use the tags keyword in the .gitlab-ci.yml file to define what runner must run the job.
deploy_job:
  stage: deploy
  script:
    - echo "Deploying on a custom runner..."
  tags:
      - deploy-runner

This example will make sure this job will run only on the deploy-runner.

10. Monitoring Pipelines

You can track your pipelines in GitLab’s interface. Look at status, logs, and performance. You can also add integrations to your monitoring solution. That makes monitoring easier. You can also use GitLab API to pull data for your dashboard.

Integrate GitLab with other monitoring tools:

  • Prometheus
  • Grafana
  • Datadog
  • New Relic
  • And any other service that supports receiving HTTP requests.

These tools will let you see how your pipeline performs.

Best Practices for GitLab CI

Now, let’s talk about the best ways to use GitLab CI effectively:

  1. Keep it Simple: Start with a simple setup. Add complexity as you need it. Simple is easy to manage.
  2. Use Version Control: Version your .gitlab-ci.yml file. It will let you track changes. This is important for long-term maintenance.
  3. Use Templates: Create templates for common jobs and stages. This way you can reuse and share configurations.
  4. Test in Isolation: Run each job in a separate Docker container. This makes the builds more reliable.
  5. Fail Fast: Make the pipeline stop right after an error. It will let you fix problems quickly.
  6. Use Meaningful Names: Use names that make sense for stages, jobs, and variables. This makes your .gitlab-ci.yml file easier to understand.
  7. Document Your Pipelines: Document the purpose of every step in the pipeline. This way it is easy for everyone to understand what the build process is about.
  8. Monitor Performance: Track the pipeline performance regularly. That way you will see areas you can optimize.
  9. Secure Your Pipelines: Keep your API keys, passwords, and tokens secure. It helps to prevent unwanted access to your services.
  10. Regularly Review and Update: As your project grows, you will have to revisit your pipeline configurations often. New technologies, new needs.

Real-World Examples

Let’s explore some examples of how to use GitLab CI for real-world projects:

Example 1: Node.js Application

This example is for a Node.js web app. It has stages for build, test, and deploy.

image: node:latest

stages:
  - build
  - test
  - deploy

cache:
  paths:
    - node_modules/

build_job:
  stage: build
  script:
    - npm install
    - npm run build
  artifacts:
    paths:
      - dist/

test_job:
  stage: test
  script:
    - npm test
  dependencies:
      - build_job

deploy_job:
  stage: deploy
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
  only:
    - main

In this example:

  • The image uses the Node.js Docker image.
  • The build saves the node modules and output directory as artifacts.
  • The deploy job uses Docker in Docker to build and push an image to GitLab Registry.
  • The deploy runs only on the main branch.

Example 2: Python Application

This example is for a Python application with testing and linting.

image: python:3.9-slim

stages:
  - lint
  - test

lint_job:
  stage: lint
  script:
    - pip install flake8
    - flake8 .

test_job:
  stage: test
  script:
    - pip install -r requirements.txt
    - python -m unittest discover

In this example:

  • We use a Python Docker image.
  • The lint_job runs Flake8.
  • The test_job runs tests with the Python unittest module.

Example 3: Infrastructure as Code (IaC)

This example checks and deploys cloud infrastructure using Terraform.

image: hashicorp/terraform:latest

stages:
  - validate
  - plan
  - apply

variables:
  TF_VAR_aws_access_key: $AWS_ACCESS_KEY
  TF_VAR_aws_secret_key: $AWS_SECRET_KEY
  TF_VAR_aws_region: "us-east-1"

before_script:
  - terraform init

validate_job:
  stage: validate
  script:
    - terraform validate

plan_job:
  stage: plan
  script:
    - terraform plan -out=tfplan
  artifacts:
    paths:
      - tfplan
  dependencies:
      - validate_job

apply_job:
  stage: apply
  script:
    - terraform apply tfplan
  dependencies:
    - plan_job
  only:
    - main

In this example:

  • We use a Terraform Docker image.
  • The validate_job checks the syntax.
  • The plan_job makes a plan. And saves it as an artifact.
  • The apply_job applies the changes. It only runs on the main branch.
  • We use GitLab variables to store AWS keys.

Troubleshooting Common Issues

Even with a good grasp of GitLab CI, issues may arise. Here are a few common problems and how to solve them:

1. Pipeline Fails with “No Runner Available”

Problem: No runner is available to run the job.

Solution:

  • Check if a runner is assigned to the project.
  • Make sure a shared runner is enabled. Or a custom runner is set up.
  • Check that the runner has the required tags.

2. Job Fails with Docker Error

Problem: A job fails because of a Docker image error.

Solution:

  • Check the name and tag of the Docker image you’re using.
  • Make sure the image exists on Docker Hub or GitLab Registry.
  • Make sure the required Docker service is configured correctly.

3. Cache Issues

Problem: The cache is not working as expected.

Solution:

  • Check if you use the same cache paths in the jobs.
  • Be sure you use the same key or a unique one.
  • Make sure there are no permissions issues on cached directories.

4. Authentication Failures

Problem: Jobs fail due to auth problems with external services.

Solution:

  • Double-check variables and secret values in the settings.
  • Make sure that tokens are correct and not expired.
  • You may have to double-check the scope of external tokens.

5. Code Not Updated

Problem: The code in a job is not the latest version.

Solution:

  • Check your repo settings to be sure all changes are saved.
  • Make sure your git configuration is correct.
  • Make sure the pipeline is triggered by commits.

6. Slow Pipelines

Problem: The pipeline takes a long time to run.

Solution:

  • Use caching and artifacts.
  • Run jobs in parallel where possible.
  • Make sure your runner is sized correctly for your task.
  • Use a cloud-based runner for faster run times.

By understanding these problems and fixes, you can handle almost any issue.

The Future of CI/CD with GitLab

The world of CI/CD is always changing. GitLab is working to improve its features. Here are some trends and features to watch:

  1. AI-Powered CI/CD: AI will help to optimize workflows, predict problems, and suggest better practices.
  2. More Cloud Integration: GitLab is always working on having better integrations with cloud providers.
  3. Enhanced Security: Focus on security in the software delivery process.
  4. Better Analytics: More advanced analytics in pipelines, you can spot inefficiencies and make informed decisions.
  5. Improved Usability: GitLab will keep making its CI/CD features easy and simple to use.

By keeping up with trends, you can use GitLab CI to its full potential.

Final Thoughts

Building a powerful CI pipeline with GitLab CI may seem difficult at first, but it’s not. By following this guide and understanding core concepts, you can easily automate your software delivery process. The power of GitLab CI is in its flexibility. It has the tools to adapt to your specific requirements. You can go from a simple test process to a complex delivery pipeline. Start with the basics, and add layers of complexity as you get better. Remember to keep learning. And always strive to improve your pipelines with each new iteration. So you don’t get caught in deployment day drama again.