DEV Community

Javad
Javad

Posted on

DevOps Tooling Masterclass: Deployment and automation — GitHub Actions vs GitLab CI/CD vs Jenkins

Hey Dev Community!
Welcome!
This blog is a part of DevOps Tooling Masterclass.


Core concepts of CI/CD and automation

  • Continuous integration: Merge early/often, run automated builds/tests, surface issues quickly.
  • Continuous delivery: Keep mainline deployable with gated promotion; delivery is a human or policy decision.
  • Continuous deployment: Auto‑promote to production once tests and gates pass; policies enforce safety.
  • Pipeline DAGs: Directed acyclic graphs representing jobs and dependencies; parallelization + artifacts glue everything.
  • Runners/executors: Isolated compute units that execute pipeline steps (VMs, containers, pods, agents).
  • Artifacts and caches: Immutable build outputs vs. reusable dependency caches for speed.
  • Environment promotion: Dev → test → staging → prod, with approvals, change windows, and rollout strategies.
  • Identity and secrets: OIDC federation, vaults, encrypted secrets, ephemeral credentials, and scoped access.

GitHub Actions deep dive

Installation and setup

  • Hosted setup: Actions are native to GitHub; no server install. Enable Actions per repo/organization.
  • Self‑hosted runners: Install the runner binary on your machine/VM/Kubernetes pod, register with repo/org, label runners for targeted jobs.
  • Workflow location: Place YAML files under .github/workflows/*.yml.

Architecture internals

  • Event‑driven engine: Workflows trigger on repository events (push, pullrequest, schedule, workflowdispatch, release). Events produce a run with an immutable snapshot (commit SHA, ref, inputs).
  • Workflow→job→step hierarchy:
    • Workflow: Top‑level file defines triggers, concurrency, permissions, env, defaults.
    • Jobs: Each job runs on a single runner; jobs can depend on other jobs (needs) forming a DAG.
    • Steps: Ordered commands or composite actions; steps share a workspace but have isolated env.
  • Actions marketplace: Reusable components (Docker actions, Node actions, composite actions). Composite actions enable in‑repo modularity without containers.
  • Secrets and permissions: Repository/Org/Env secrets; fine‑grained token permissions (permissions: block) minimizing default GITHUB_TOKEN scopes.
  • OIDC identity: Federated identity to cloud providers (AWS STS, Azure Workload Identity, GCP Workload Identity) for short‑lived, no‑secret deployments.
  • Caching vs artifacts:
    • Cache: Keyed dependency reuse (e.g., npm, Maven).
    • Artifacts: Immutable build outputs for cross‑job sharing or post‑run download.
  • Concurrency controls: concurrency: groups cancel superseded runs (e.g., one per branch), preventing noisy redeploys.
  • Matrix builds: Cartesian combinations (OS, language versions, architectures) for broad test coverage.
  • Reusable workflows: workflow_call lets you centralize pipeline logic across repos (true org‑level DRY).

Suitable for

  • Small/medium teams: Fast onboarding, minimal ops.
  • Open‑source projects: PR‑centric flows, marketplace leverage.
  • Startups: Rapid iteration, OIDC to clouds, infra‑light.

Pros

  • Tight SCM integration: PR checks, code reviews, environments, status checks.
  • Developer velocity: Simple YAML, marketplace actions, matrix builds.
  • Secure identity: OIDC removes long‑lived cloud keys.

Cons

  • Complex enterprise topology: Large multi‑repo orchestration can get fragmented without reusable workflows discipline.
  • Vendor coupling: Deeply GitHub‑centric; multi‑SCM strategies need workarounds.

Beginner: first CI and static site deployment

yaml
name: CI
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install deps
run: npm ci
- name: Unit tests
run: npm test -- --ci

Deploy to GitHub Pages:

yaml
name: Deploy Pages
on:
workflow_dispatch:
jobs:
build-deploy:
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci && npm run build
- uses: actions/upload-pages-artifact@v3
with: { path: ./dist }
- uses: actions/deploy-pages@v4

Intermediate: matrix tests, caching, OIDC to AWS

yaml
name: Node CI (matrix + cache)
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ matrix.node }} }
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('/package-lock.json') }}
restore-keys: npm-${{ runner.os }}-${{ matrix.node }}-
- run: npm ci && npm test -- --ci

Deploy to AWS with OIDC:

yaml
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-oidc-deployer
aws-region: us-east-1
- run: aws s3 sync dist s3://my-bucket --delete

Advanced: multi‑env promotion, canary via Kubernetes

`yaml
name: CD Promotion
on:
workflow_dispatch:
inputs:
version: { required: true }
env:
IMAGE: ghcr.io/org/app:${{ inputs.version }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t $IMAGE .
- run: echo $CR_PAT | docker login ghcr.io -u org --password-stdin
- run: docker push $IMAGE
outputs:
image: ${{ env.IMAGE }}

deploy-dev:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v3
- run: |
kubectl set image deploy/app app=${{ needs.build.outputs.image }}
kubectl rollout status deploy/app

deploy-canary:
needs: deploy-dev
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- run: |
kubectl apply -f k8s/canary.yaml
kubectl rollout status deploy/app-canary

promote-prod:
needs: deploy-canary
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- run: kubectl apply -f k8s/prod.yaml
`

Key internals at this level:

  • Environment protection: Required reviewers for prod deploys; environment: gates.
  • Concurrency/cancellation: Prevent overlapped releases per env.
  • Reusable workflow orchestration: A central release workflow called by app repos.

GitLab CI/CD deep dive

Installation and setup

  • SaaS: GitLab.com includes CI/CD; no server install.
  • Self‑managed: Install GitLab and register GitLab Runners.
  • Executors: Shell, Docker, Docker Machine (autoscale), Kubernetes, custom.
  • Pipeline definition: .gitlab-ci.yml at repo root; project/instance templates enable reuse.

Architecture internals

  • Stages and jobs: stages: create ordered phases; jobs run in parallel within a stage; job needs: allows DAG across stages (advanced).
  • Runners and executors: Runners pick queued jobs; executors provide isolation (containers/pods/VMs). Kubernetes executor creates ephemeral pods per job.
  • Artifacts/caches:
    • Artifacts: Immutable outputs with expiry; scoped to downstream jobs.
    • Caches: Reusable dependency layers keyed by content.
  • Environments/deployments: Track deploys per environment; show last commit, URL, and rollout status; supports manual actions and approvals.
  • Auto DevOps: Opinionated pipeline templates (build/test/containerize/scan/deploy) with Kubernetes integration.
  • Security scanning: SAST, DAST, dependency scans, container scans, license scans—first‑class jobs.
  • Policies and approvals: Protected environments, required approvers, change management integration.

Suitable for

  • Medium/large teams: All‑in‑one DevOps suite.
  • Kubernetes‑native orgs: Tight K8s and Helm integration.
  • Compliance‑heavy orgs: Built‑in security and audit trails.

Pros

  • End‑to‑end platform: SCM, CI/CD, registry, security, environments in one place.
  • Powerful runners/executors: Kubernetes autoscaling support.
  • Security built‑in: Turnkey scans and reports.

Cons

  • Operational complexity: Self‑managed runners at scale need solid infra chops.
  • Learning curve: Rich features require disciplined structure.

Beginner: basic pipeline

`yaml
stages: [build, test, deploy]

build:
stage: build
image: node:20
script:
- npm ci
- npm run build
artifacts:
paths: [dist]

test:
stage: test
image: node:20
dependencies: [build]
script:
- npm test -- --ci

deploy:
stage: deploy
image: alpine:3.19
environment:
name: staging
url: https://staging.example.com
script:
- echo "Deploy dist to staging"
when: manual
`

Intermediate: Docker registry and canary

`yaml
variables:
IMAGETAG: $CIREGISTRYIMAGE:$CICOMMIT_SHA

stages: [build, test, push, deploy]

build:
stage: build
image: docker:24
services: [docker:24-dind]
script:
- docker build -t $IMAGE_TAG .
- echo $CIREGISTRYPASSWORD | docker login -u $CIREGISTRYUSER $CI_REGISTRY --password-stdin
- docker push $IMAGE_TAG
artifacts:
reports:
dotenv: build.env
when: always

deploy_canary:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: canary
url: https://canary.example.com
script:
- kubectl set image deploy/app app=$IMAGE_TAG
- kubectl rollout status deploy/app
needs: [build]
`

Advanced: DAG pipelines, Helm, approvals, scans

`yaml
stages: [lint, build, test, security, package, deploy]

lint:
stage: lint
script: [ "npm run lint" ]

build:
stage: build
script: [ "npm ci", "npm run build" ]
artifacts:
paths: [dist]

unit_tests:
stage: test
needs: [build]
script: [ "npm test -- --ci" ]

sast:
stage: security
needs: [build]
artifacts:
reports:
sast: gl-sast-report.json
script:
- ./scripts/run-sast.sh

package:
stage: package
needs: [unit_tests, sast]
script:
- helm package helm/chart
artifacts:
paths: [chart-*.tgz]

deploy_staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
needs: [package]
script:
- helm upgrade --install app ./chart-0.1.0.tgz -f values/staging.yaml
when: manual
rules:
- if: '$CIPIPELINESOURCE == "pipeline"'

deploy_prod:
stage: deploy
environment:
name: production
url: https://example.com
needs: [deploy_staging]
script:
- helm upgrade --install app ./chart-0.1.0.tgz -f values/prod.yaml
when: manual
approvals:
users: [ "ops-lead", "security-lead" ]
`

Key internals at this level:

  • DAG + needs: True parallel orchestration across stages for optimal throughput.
  • Protected environments: Gate deployments with approvals and protected branches/tags.
  • Security reports: Standardized artifact formats enable dashboards and MR decorations.
  • Kubernetes executor: Ephemeral pods for jobs; auto scale runners to match load.

Jenkins deep dive

Installation and setup

  • Controller (master): Host Jenkins via Docker, VM, or Kubernetes.
  • Agents: Static (SSH) or dynamic (cloud/Kubernetes). Agents execute builds; label agents for targeting.
  • Plugins: Core extensibility: Git, GitHub/GitLab, Pipeline, Credentials, Kubernetes, Blue Ocean, Configuration‑as‑Code (JCasC), Credentials Binding, Terraform, Ansible, Artifactory, etc.

Quick start (Docker):

bash
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts

Architecture internals

  • Queue and executors: Jobs enter a centralized queue; executors on agents pull work based on labels; throttling plugins manage concurrency.
  • Remoting protocol (JNLP): Agents connect to controller securely; file transfers and logs stream over remoting.
  • Pipeline types:
    • Freestyle: UI‑configured jobs (legacy/simple).
    • Declarative pipeline: Structured YAML‑like Groovy; opinionated for readability.
    • Scripted pipeline: Full Groovy DSL, any control flow, maximum power.
  • Shared libraries: Versioned pipeline libraries injected at runtime (@Library('repo-lib') _), enabling true DRY pipelines.
  • Credentials: Scoped and masked; credentials binding into environment variables for steps; secrets never appear in logs (if masked correctly).
  • Configuration as Code (JCasC): YAML configs define controllers/plugins/credentials; reproducible infrastructure.

Suitable for

  • Enterprises: Complex, legacy, hybrid estates needing maximal customization.
  • Heavily regulated: Tight control over infra and data locality.
  • Multi‑SCM/multi‑cloud: Universal integrations.

Pros

  • Unlimited flexibility: Anything is possible with scripted pipelines and plugins.
  • Mature ecosystem: Vast plugin library, battle‑tested patterns.
  • Infra control: Fit exactly to your network, security, and performance constraints.

Cons

  • Maintenance overhead: Upgrades, plugin compatibility, controller HA.
  • Complexity risk: Scripted pipelines can become snowflakes without governance.

Beginner: simple declarative pipeline

groovy
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'npm ci && npm run build'
}
}
stage('Test') {
steps {
sh 'npm test -- --ci'
}
}
}
}

Intermediate: Dockerized builds and multi‑branch

groovy
pipeline {
agent { label 'docker' }
stages {
stage('Build in Docker') {
steps {
sh 'docker build -t app:${BUILD_NUMBER} .'
}
}
stage('Push') {
environment {
REGUSER = credentials('reguser')
REGPASS = credentials('regpass')
}
steps {
sh '''
echo $REGPASS | docker login -u $REGUSER registry.example.com --password-stdin
docker push registry.example.com/app:${BUILD_NUMBER}
'''
}
}
}
}

Multi‑branch: Jenkins scans SCM and creates pipelines per branch/PR, running Jenkinsfile in context.

Advanced: scripted pipeline, canary, Kubernetes agents, libraries

`groovy
@Library('pipelines-lib') _
def version = params.VERSION ?: env.GIT_COMMIT

pipeline {
agent none
options { timeout(time: 60, unit: 'MINUTES') }
stages {
stage('Build') {
agent { label 'docker' }
steps {
sh "docker build -t registry/app:${version} ."
sh "docker push registry/app:${version}"
}
}
stage('Deploy Canary') {
agent { label 'k8s' }
steps {
sh """
kubectl set image deploy/app app=registry/app:${version}
kubectl rollout status deploy/app
"""
}
}
stage('Promote Prod') {
when { expression { env.BRANCH_NAME == 'main' } }
input { message 'Approve production deployment?' }
agent { label 'k8s' }
steps {
sh "kubectl apply -f k8s/prod.yaml"
}
}
}
post {
always {
junit 'reports/*.xml'
archiveArtifacts artifacts: 'dist/', fingerprint: true
}
}
}
`

Key internals at this level:

  • Kubernetes plugin: Spawns ephemeral agent pods per stage; labels select pod templates.
  • Shared libraries: Centralize deployment logic; versioned and tested like application code.
  • Governance: JCasC + seed jobs reproduce Jenkins; policy plugins limit unsafe steps.

Side‑by‑side comparison

Attribute GitHub Actions GitLab CI/CD Jenkins
Setup Native in GitHub; self‑hosted runners optional Native in GitLab; runners required for scale Controller + agents; plugins
Pipeline model Workflow → jobs → steps; DAG via needs Stages/jobs; DAG via needs Declarative/scripted Groovy; full control
Executors Hosted runners, self‑hosted, containers Shell, Docker, Kubernetes, autoscaling Static/dynamic agents, Kubernetes, cloud
Identity GITHUB_TOKEN, OIDC federation Project tokens, CI variables, Vault integrations Credentials store, bindings, vault plugins
Security Fine‑grained permissions; environments Built‑in SAST/DAST; protected envs Plugin‑based; strong but DIY configuration
Scaling Matrix builds, reusable workflows Runner autoscaling; K8s executor Horizontal agents; controller HA needed
Best fit Open‑source, startups, GitHub‑centric Kubernetes‑heavy, mid/large orgs Enterprises, bespoke/legacy/hybrid

Sources: Internal expertise and platform behaviors observed in production environments.


Advanced deployment scenarios and full examples

Blue/green with GitHub Actions (two active environments)

`yaml
name: Blue-Green
on:
workflow_dispatch:
inputs:
target: { required: true, type: choice, options: [blue, green] }
env:
IMAGE: ghcr.io/org/app:${{ github.sha }}

jobs:
build_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t $IMAGE .
- run: echo $CR_PAT | docker login ghcr.io -u org --password-stdin
- run: docker push $IMAGE

deploy_target:
needs: build_push
runs-on: ubuntu-latest
steps:
- uses: azure/setup-kubectl@v3
- run: |
kubectl -n prod set image deploy/app-${{ inputs.target }} app=$IMAGE
kubectl -n prod rollout status deploy/app-${{ inputs.target }}

switch_traffic:
needs: deploy_target
runs-on: ubuntu-latest
steps:
- run: kubectl -n prod apply -f k8s/ingress-${{ inputs.target }}.yaml
`

Canary with GitLab CI/CD (progressive rollout and metrics gate)

`yaml
stages: [build, deploy, verify, promote]

build:
stage: build
image: docker:24
services: [docker:24-dind]
script:
- docker build -t $CIREGISTRYIMAGE:$CICOMMITSHA .
- docker push $CIREGISTRYIMAGE:$CICOMMITSHA
artifacts:
reports:
dotenv: build.env

deploy_canary:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: canary
script:
- kubectl -n prod set image deploy/app app=$CIREGISTRYIMAGE:$CICOMMITSHA
- kubectl -n prod rollout status deploy/app

verify_slo:
stage: verify
image: curlimages/curl:8.4.0
script:
- curl -s https://metrics.example.com/slo | grep "error_rate<0.5%"
allow_failure: false
needs: [deploy_canary]

promote_prod:
stage: promote
environment:
name: production
when: manual
script:
- kubectl -n prod apply -f k8s/ingress-prod.yaml
`

Rolling updates with Jenkins (Kubernetes agents)

groovy
pipeline {
agent none
stages {
stage('Build & Push') {
agent { label 'docker' }
steps {
sh "docker build -t registry/app:${env.GIT_COMMIT} ."
sh "docker push registry/app:${env.GIT_COMMIT}"
}
}
stage('Rolling Update') {
agent { label 'k8s' }
steps {
sh """
kubectl -n prod set image deploy/app app=registry/app:${env.GIT_COMMIT}
kubectl -n prod rollout status deploy/app --timeout=5m
"""
}
}
}
}


Security, identity, secrets, and compliance

  • OIDC and short‑lived credentials:

    • GitHub Actions: Prefer OIDC to AWS/Azure/GCP over static keys. Rotate roles, scope permissions tightly (permissions:).
    • GitLab CI: Use CI variables + cloud Workload Identity; integrate with HashiCorp Vault for dynamic secrets.
    • Jenkins: Store secrets in Credentials and bind at step scope; adopt Vault plugins for ephemeral secrets.
  • Least privilege and scopes:

    • GitHub: Restrict GITHUB_TOKEN; use environment‑protected deployments with approvals.
    • GitLab: Protected branches/tags; environment‑level approvals; fine‑grained runner access.
    • Jenkins: Role‑based authorization; restrict script approvals; lock down agent capabilities.
  • Security scanning:

    • GitHub: CodeQL and marketplace scanners; dependency review gates.
    • GitLab: First‑class SAST/DAST/container/license scans; merge request reports.
    • Jenkins: Integrate scanners via plugins; publish results with junit and HTML reports.
  • Auditability:

    • GitHub/GitLab: Built‑in audit logs of runs, approvals, environment changes.
    • Jenkins: Enable audit plugins; centralize logs; route to SIEM.

Performance, scaling, and cost management

  • Runner/executor autoscaling:

    • GitHub Actions: Prefer hosted runners for elasticity; use self‑hosted only for special hardware. Kubernetes self‑hosted runners for burst workloads.
    • GitLab CI: Docker Machine autoscaling or Kubernetes executor; tag runners by capability; isolate heavy jobs.
    • Jenkins: Kubernetes agents with pod templates; cloud agents (EC2/GCE) via plugins; scale executors per queue depth.
  • Caching and artifacts:

    • Cache keys: Hash lockfiles; fallback restore keys for resilience.
    • Artifact size and TTL: Keep artifacts small; set expirations; archive only essentials.
  • Parallelization:

    • Matrix/DAG: Aggressive parallel testing reduces wall‑clock time.
    • Shard test suites: Split by file glob or dynamic history to balance load.
  • Throughput controls:

    • GitHub: concurrency: to cancel superseded runs; max-parallel on matrices.
    • GitLab: Runner concurrency; resource_group for serialized deploys.
    • Jenkins: Throttle concurrent builds per project; lockable resources for shared infra.
  • Cost discipline:

    • Hosted minutes: Optimize caches; avoid unnecessary rebuilds; schedule heavy jobs off‑peak.
    • Self‑hosted infra: Right‑size nodes; spot instances for CI; autoscale down aggressively.

Decision guide and recommendations

  • If you’re GitHub‑centric and want velocity with minimal ops: Choose GitHub Actions, lean on reusable workflows, OIDC, and environments with approvals. Use hosted runners + small set of self‑hosted labels for special cases (GPU, large memory).

  • If you want an integrated DevSecOps suite with Kubernetes‑native workflows: Choose GitLab CI/CD. Use Kubernetes executor for ephemeral jobs, enable SAST/DAST/container scans, and run Helm‑based deployments with protected environments and approvals.

  • If you need maximal flexibility across hybrid/legacy systems, and you can invest in governance: Choose Jenkins. Adopt declarative pipelines + shared libraries, JCasC for reproducibility, Kubernetes agents for elasticity, and strict RBAC + credentials policies.

  • General principles:

    • Start simple, scale intentionally: Establish pipeline standards and libraries early.
    • Secure by default: Ephemeral identities, least privilege, required approvals for prod.
    • Observe everything: Surface build/test/deploy metrics; gate promotions by SLOs.
    • DRY pipelines: Centralize common steps as actions, templates, or libraries.

Appendix: quick recipes

GitHub Actions reusable workflow

`yaml

.github/workflows/reusable-ci.yml
name: Reusable CI
on:
workflow_call:
inputs:
node:
required: true
type: string
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node }} }
- run: npm ci && npm test -- --ci
`

GitLab CI template include

`yaml

.gitlab-ci.yml
include:

  • project: 'org/ci-templates' file: '/node-ci.yml' `

Jenkins shared library usage

groovy
@Library('company-ci') _
pipeline {
agent any
stages {
stage('CI') {
steps {
ciNodeSteps(nodeVersion: '20')
}
}
}
}


I hope you enjoy the series of DevOps Tools Masterclass, be updated, because every minutes we are publish a new blogs 😅
Have nice times!

Top comments (1)

Collapse
 
javadinteger profile image
Javad • Edited

This post is the second entry in the DevOps Tooling Masterclass series 🚀
Stay tuned for the next one