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)
This post is the second entry in the
DevOps Tooling Masterclassseries 🚀Stay tuned for the next one