メインコンテンツまでスキップ

Gitlab CI, Zero to Hero

· 約6分
Mikyan
白い柴犬

.gitlab-ci.yml file is the entry point of GitLab CI/CD, must be placed in your repository root.

Job, Stage, Pipeline

Job: Individual tasks that run commands, build blocks of Gitlab CI pipeline.

Pipeline: The top-level component containing all jobs. However there is no way to directly define a pipeline, user can only define jobs, Gitlab constructs pipeline on:

  • Which jobs match the trigger conditions
  • How those jobs are related

Common Patterns for pipeline separation:

  • by branch
  • by pipeline source
  • by variables

Stage: groups of jobs that run in sequence

Stages are a grouping mechanism for jobs They define execution sequence

GitLab have a default stages definition. Or you can define stages in .gitlab-ci.yml. GitLab just enforces the execution order you specify.

# If you omit the stages: declaration
# GitLab uses these DEFAULT stages:
# - .pre
# - build
# - test
# - deploy
# - .post

stages:
- prepare
- build
- test
- deploy

GitLab Runners

Gitlab runner is a software (agent) that execute your CI/CD jobs

Listener/worker, that constantly polls GitLab for jobs, execute job, report result.

Gitlab Runners are installed on VM/container/server/kubernetes Pod

Type of runners:

  • Shared Runners: Available to all projects
  • Group Runners: Available to all projects in a group
  • Specific Runners: Dedicated to specific projects

Runners use Executors to run your job.

Executor Types:

  • shell-job
  • docker-job
  • k8s-job

default is docker executor

each job runs in a brand new container. for the following reasons:

  • isolation
  • predictability
Pipeline Start

[build-job]
├─ Pull node:16 image
├─ Create new container
├─ Run scripts
└─ Destroy container ❌

[test-job]
├─ Pull node:16 image (cached)
├─ Create NEW container
├─ Run scripts (no files from build!)
└─ Destroy container ❌

Pipeline End

Sharing data between jobs: artifacts

Gitlabe automatically clones your repository before any job.

stages:
- install
- build
- test
- deploy

install-deps:
stage: install
image: node:16
script:
- npm ci
- npm audit
artifacts:
paths:
- node_modules/
expire_in: 1 day

build-app:
stage: build
image: node:16
script:
- npm run build
dependencies:
- install-deps # Gets node_modules/
artifacts:
paths:
- dist/
- node_modules/

test-unit:
stage: test
image: node:16 # Fresh container!
script:
- npm test
dependencies:
- build-app # Gets dist/ and node_modules/

test-e2e:
stage: test
image: cypress/included:10 # Different image, fresh container
services:
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
alias: app
script:
- cypress run --config baseUrl=http://app:3000
dependencies:
- build-app

deploy:
stage: deploy
image: alpine:latest # Fresh container
script:
- apk add --no-cache curl
- curl -X POST $DEPLOY_WEBHOOK
dependencies:
- build-app # Only needs dist/

Cache: Speed optimization (might exist, might not)

Artifacts: guaranteed data transfer between jobs

If Job B needs Job A → B gets A's artifacts If Job B doesn't reference A → B can NEVER get A's artifacts

Cache is a way to preserve files between pipeline runs to speed up the jobs. cache is about preserving data across different pipelines.

Basic Job Configuration

Script types:

  • script: the only required field in a job. containing main job logic
    • commands run in sequence
    • if any command fails, job fails
    • runs in the project directory
    • has access to all CI variables
  • before_script: run before the main script, Perfect for environment setup
  • after_script: run after the main script, even if the job fails

why we need three scripts:

  • separation of concerns
  • guaranteed cleanup
  • error handling simplicity

Configurations control when jobs run

  • only/except: legacy, but still in use, specify the branch

  • when: specify the execution timing, can be used standalone or within rules

    • on_success: run if all previous succeeded
  • rules (recomended):

    • changes: File Changes:
    • exists: File existence
    • allow_failure: in rules
    • variables: Set variables in rules
  • trigger: use to start a completely separate pipeline. either in the same project or a different project

job:
rules:
# Branch conditions
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH != "main"'
- if: '$CI_COMMIT_BRANCH =~ /^feature-/'

# Tag conditions
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_TAG =~ /^v\d+/'

# Pipeline source
- if: '$CI_PIPELINE_SOURCE == "push"'
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_PIPELINE_SOURCE == "schedule"'

# Complex conditions
- if: '$CI_COMMIT_BRANCH == "main" && $DEPLOY_ENABLED == "true"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" || $CI_MERGE_REQUEST_LABELS =~ /urgent/'

Keywords:

stage: image: specify which docker image to use services: Helper containers variables: environment variables artifacts: save job output dependencies: control artifact flow needs: DAG (Directed Acyclic Graph) rules: smart conditions (better than only / except) when: execution conditions cache: speed up pipelines retry: handle flaky jobs timeout: prevent hanging allow_failure: non-blocking failures coverage: extract coverage environment: track deployments parallel: multiple instances trigger: pipeline control only

tags

Job Status Types:

pending/running/success/failed/skipped/manual

failed: script returned non-zero exit code skipped means the didn't meet conditions manual: waiting for manual trigger

CI/CD variables:

key-value pairs available during job execution. Environment variables on steroids

  1. Predefined variables (GitLab Provides)
  • CI_PROJECT_NAME
  • CI_COMMIT_BRANCH
  • CI_COMMIT_SHA
  • CI_COMMIT_MESSAGE
  • CI_PIPELINE_ID
  • CI_PIPELINE_SOURCE
  • CI_JOB_ID
  • CI_JOB_NAME
  • CI_RUNNER_ID
  • CI_RUNNER_TAGS
  • CI_PROJECT_URL
  • CI_PIPELINE_URL
  • CI_JOB_URL
deploy:
script:
# Different behavior for different triggers
- |
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
echo "This is a merge request"
fi

# Use commit info
- docker build -t myapp:$CI_COMMIT_SHORT_SHA .

# Conditional logic
- |
if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
echo "On default branch (usually main)"
fi

Variables can be defined at multiple levels, Higher level wins.

Job -> Pipeline -> Project -> Group -> Instance -> Predefined

  • protected variables
  • masked variables
  • file variables
  • variable expansion
build:
variables:
VERSION: "1.0.0"
IMAGE_TAG: "myapp:$VERSION" # Expands to myapp:1.0.0

Conditional variables

deploy:
script:
- echo "Deploying to $ENVIRONMENT"
rules:
- if: $CI_COMMIT_BRANCH == "main"
variables:
ENVIRONMENT: production
REPLICAS: "5"
- if: $CI_COMMIT_BRANCH == "develop"
variables:
ENVIRONMENT: staging
REPLICAS: "2"

Dynamic variables

build:
script:
# Create variables during job
- export BUILD_TIME=$(date +%Y%m%d-%H%M%S)
- echo "VERSION=$CI_COMMIT_SHORT_SHA-$BUILD_TIME" >> build.env
artifacts:
reports:
dotenv: build.env # Pass to next jobs

test:
needs: [build]
script:
- echo "Testing version: $VERSION" # From build.env

Needs

let you create DAG instead of rigid stage-based pipelines.