← कोर्सच्या मुख्य पानाकडे परत

🗣️ एक pipeline, चार बोली

चार कुरिअर कंपन्या, तोच मार्ग, वेगळे फॉर्म. खालची प्रत्येक फाइल repo मध्ये आहे आणि त्याच conveyor चे वर्णन करते: cache आणि JUnit अहवालासह तीन Node आवृत्त्यांवर चाचणी → commit SHA ने tag केलेली image build, smoke-test आणि push → staging वर deploy → माणूस मंजुरी देतो → production. धडा 11 तक्ता समजावतो; धडे 03–10 प्रत्येक तुकडा चारही बोलींमध्ये दाखवतात.

🪨 Rosetta तक्ता

एक ओळ आडवी वाचा आणि तुम्हाला भेटणारी कोणतीही pipeline भाषांतरित करता येईल. keyword खऱ्या फाइली वापरतात तेच आहेत.

संकल्पना⚙️ GitHub Actions🔄 CircleCI🦊 GitLab CI🎩 Jenkins
Config फाइल.github/workflows/*.yml.circleci/config.yml.gitlab-ci.ymlJenkinsfile
Trigger 🔔on: push / pull_requestdefault प्रत्येक push; filters: ने संकुचित कराdefault प्रत्येक push; rules: if: ने संकुचित कराप्रत्येक push (multibranch); when { branch } ने संकुचित करा
संपूर्ण runworkflowworkflowpipelinepipeline
कामाचे एककjob (runs-on)job (docker: executor)stage मधील jobstage (+ agent)
एक कामsteps: - run: / uses:steps: - run: / orb आज्ञाएक script: ओळsteps { sh '…' }
यंत्रrunner (ubuntu-latest)executor (cimg/node:22.12)runner (image: node:22-alpine)agent (docker { image })
Matrix 📏strategy: matrix:matrix: parameters:parallel: matrix:matrix { axes { … } }
Cache 🗄️actions/cache, key hashFiles() नेrestore_cache / save_cache, key checksum नेcache: key: files:built-in नाही — टिकणारा workspace किंवा Job Cacher plugin
Artifact 📎upload-artifact / download-artifactstore_artifacts / persist_to_workspaceartifacts: paths:archiveArtifacts / stash
चाचणी अहवाल 📋JUnit XML artifact म्हणून upload कराstore_test_resultsartifacts: reports: junit (MR widget)junit पायरी
Secrets 🔐secrets.X (झाकलेले) · vars.X (उघड)project env var · context: प्रत्येक job लाCI/CD variables (झाकलेले, संरक्षित)Credentials + withCredentials / withAWS
Cloud ओळख 🪪permissions: id-token: write + configure-aws-credentialsaws-cli/setup with role_arnid_tokens: + assume-role-with-web-identityplugin किंवा agent वरील instance role
फक्त main वरon: push: branches: [main]filters: branches: only: mainrules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHwhen { branch 'main' }
मंजुरीचे गेट 🛑environment: + required reviewerstype: approvalwhen: manualinput पायरी
Environmentsenvironment: staging / productioncontext + approval jobenvironment: name:stage (plugin आणखी देतात)
🧠 काय बदलत नाही ते पहा: चारही node prepare.js, node --test आणि docker build … && docker push चालवतात. logic script आणि package.json मध्ये ठेवा, YAML पातळ ठेवा, म्हणजे विक्रेता बदलणे हे पुनर्लेखन न राहता भाषांतर होते.

📄 चार फाइली, शेजारी शेजारी

📄 .github/workflows/ci.yml

# ✅ The checking desk (lessons 03–05): runs on EVERY push and pull request.
# Fork this repo, push a commit, and watch this go green (or red) in the Actions tab.
name: ✅ ci

on:
  push:
  pull_request:

permissions:
  contents: read            # lesson 06: the job's own token gets the least it needs

concurrency:                # a newer push cancels the older run of the same branch
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: test (node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false      # lesson 04: let all three rulers finish, even if one fails
      matrix:
        node: [20, 22, 24]  # three rulers — the same homework checked three ways
    defaults:
      run:
        working-directory: app
    steps:
      - name: 📥 checkout
        uses: actions/checkout@v4

      - name: 🟢 node ${{ matrix.node }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

      - name: 🗄️ cache the slow preparation (lesson 04)
        id: cache
        uses: actions/cache@v4
        with:
          path: app/prepared
          key: prepared-${{ hashFiles('app/prepare.js') }}   # change prepare.js → new key → miss

      - name: ⏳ prepare (slow on a miss, skipped on a hit)
        if: steps.cache.outputs.cache-hit != 'true'
        run: node prepare.js

      - name: 🧪 test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml

      - name: 📎 upload the report card (lesson 05)
        if: always()          # especially when tests FAIL — that's when you need the report
        uses: actions/upload-artifact@v4
        with:
          name: test-results-node${{ matrix.node }}
          path: app/test-results.xml
          retention-days: 7

📄 .github/workflows/ship.yml

# 📦 The photocopier (lesson 07): build the image, prove it runs, push it to ECR
# tagged with the commit SHA — then hand it to the delivery van (deploy.yml).
#
# Runs on every push to main. The PUSH and DEPLOY jobs only run when the repo has the
# variables below (Settings → Secrets and variables → Actions → Variables); a plain
# fork stays green and simply skips them:
#   AWS_ROLE_ARN   arn:aws:iam::123456789012:role/learn-cicd-school-ci   (iam/ shows the role)
#   AWS_REGION     ap-south-1
#   ECR_REPOSITORY hello-courier
#   EKS_CLUSTER    school-eks                                            (deploy.yml only)
name: 📦 ship

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  id-token: write           # lesson 06: lets this run ask for an OIDC badge — no stored AWS keys

jobs:
  test:
    runs-on: ubuntu-latest
    defaults: { run: { working-directory: app } }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: node --test

  build:
    name: build & smoke-test the image
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 🍱 docker build (version label = short SHA)
        run: docker build --build-arg APP_VERSION=${GITHUB_SHA::7} -t hello-courier:${GITHUB_SHA} app
      - name: 🧪 test the BOX, not just the code
        run: |
          docker run -d --rm -p 3000:3000 --name smoke hello-courier:${GITHUB_SHA}
          for i in $(seq 1 20); do curl -fsS localhost:3000/healthz && break; sleep 0.5; done
          curl -fsS localhost:3000/ | grep -q "version ${GITHUB_SHA::7}"
          docker stop smoke
      - name: 💾 keep the image for the push job (same run, different machine)
        run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz
      - uses: actions/upload-artifact@v4
        with: { name: image, path: image.tar.gz, retention-days: 1 }

  push:
    name: push to ECR
    needs: build
    if: vars.AWS_ROLE_ARN != ''            # skipped on forks without AWS — still green
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.meta.outputs.image }}
    steps:
      - uses: actions/download-artifact@v4
        with: { name: image }
      - run: docker load < image.tar.gz

      - name: 🪪 show the badge, get a day pass (OIDC → temporary credentials)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}

      - name: 🎫 log in to ECR
        id: ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: 🏷️ tag with the commit SHA and push
        id: meta
        env:
          IMAGE: ${{ steps.ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ github.sha }}
        run: |
          docker tag hello-courier:${GITHUB_SHA} "$IMAGE"
          docker push "$IMAGE"
          echo "image=$IMAGE" >> "$GITHUB_OUTPUT"

  deploy:
    name: 🚚 deliver
    needs: push
    if: vars.EKS_CLUSTER != ''
    permissions:
      contents: read
      id-token: write
    uses: ./.github/workflows/deploy.yml   # lesson 08–10: staging → approval → production
    with:
      image: ${{ needs.push.outputs.image }}

📄 .github/workflows/deploy.yml

# 🚚 The delivery van (lessons 08–10): staging first, then a human signs, then production.
#
# Called by ship.yml after a successful push, or started by hand (Actions → 🚚 deploy → Run).
# The "production" environment must be created once in Settings → Environments with
# "Required reviewers" — THAT checkbox is the approval gate (lesson 08). Nothing here
# deploys until a named human clicks Approve.
name: 🚚 deploy

on:
  workflow_call:
    inputs:
      image:
        description: full image reference, e.g. 123456789012.dkr.ecr.ap-south-1.amazonaws.com/hello-courier:<sha>
        required: true
        type: string
  workflow_dispatch:
    inputs:
      image:
        description: full image reference to deploy
        required: true
        type: string

permissions:
  contents: read
  id-token: write

jobs:
  staging:
    runs-on: ubuntu-latest
    environment: staging                          # the practice notice board
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with: { role-to-assume: "${{ vars.AWS_ROLE_ARN }}", aws-region: "${{ vars.AWS_REGION }}" }
      - name: 🔧 point kubectl at the cluster
        run: aws eks update-kubeconfig --region "${{ vars.AWS_REGION }}" --name "${{ vars.EKS_CLUSTER }}"
      - name: 📌 pin the new notice (rolling update, lesson 09)
        run: |
          sed -i "s|IMAGE_PLACEHOLDER|${{ inputs.image }}|" k8s/deployment.yaml
          kubectl apply -n staging -f k8s/
          kubectl rollout status -n staging deployment/hello-courier --timeout=180s
      - name: 🧪 smoke-test staging from inside the cluster
        run: kubectl run -n staging smoke --rm -i --restart=Never --image=curlimages/curl -- -fsS http://hello-courier/healthz

  production:
    needs: staging
    runs-on: ubuntu-latest
    environment: production                       # 🛑 required reviewers = the principal's signature
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with: { role-to-assume: "${{ vars.AWS_ROLE_ARN }}", aws-region: "${{ vars.AWS_REGION }}" }
      - run: aws eks update-kubeconfig --region "${{ vars.AWS_REGION }}" --name "${{ vars.EKS_CLUSTER }}"
      - name: 📌 roll it out to the main board
        run: |
          sed -i "s|IMAGE_PLACEHOLDER|${{ inputs.image }}|" k8s/deployment.yaml
          kubectl apply -n production -f k8s/
          kubectl rollout status -n production deployment/hello-courier --timeout=180s
      # rollback = kubectl rollout undo deployment/hello-courier -n production   (lesson 09)
      # …or, better, re-run this workflow with yesterday's image tag: the tag IS the version.

📄 .circleci/config.yml

# 🔄 The SAME pipeline in the CircleCI dialect (lesson 11).
#   test (matrix) → build-and-push (context = the badge) → hold-for-approval → deploy-to-eks
# Modeled on a real pipeline: only the jobs that talk to AWS receive the context (least privilege).
# The context `school-cicd` must provide AWS_ROLE_ARN (OIDC) or AWS keys, AWS_ACCOUNT_ID,
# AWS_DEFAULT_REGION, ECR_REPOSITORY, EKS_CLUSTER.
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@5.1        # `aws-cli/setup` with role_arn = OIDC, no stored keys

parameters:
  node-versions:
    type: string
    default: "20.18,22.12,24.0"

jobs:
  test:
    parameters:
      node:
        type: string
    docker:
      - image: cimg/node:<< parameters.node >>
    working_directory: ~/repo/app
    steps:
      - checkout:
          path: ~/repo
      - restore_cache:                                        # lesson 04: the drawer
          keys:
            - prepared-v1-{{ checksum "prepare.js" }}
      - run:
          name: ⏳ prepare (skipped when restored from cache)
          command: node prepare.js
      - save_cache:
          key: prepared-v1-{{ checksum "prepare.js" }}
          paths: [prepared]
      - run:
          name: 🧪 test
          command: |
            mkdir -p test-results
            node --test --test-reporter=spec --test-reporter-destination=stdout \
                        --test-reporter=junit --test-reporter-destination=test-results/junit.xml
      - store_test_results:                                   # lesson 05: the report card, parsed
          path: test-results
      - store_artifacts:                                      # …and the raw file, downloadable
          path: test-results

  build-and-push:
    docker:
      - image: cimg/aws:2024.03          # AWS CLI + Docker CLI preinstalled
    steps:
      - checkout
      - setup_remote_docker              # a Docker engine for this job to build with
      - aws-cli/setup:                   # 🪪 OIDC badge → temporary credentials
          role_arn: ${AWS_ROLE_ARN}
          region: ${AWS_DEFAULT_REGION}
      - run:
          name: 🍱 build, smoke-test, tag with the commit SHA, push
          command: |
            REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com"
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
            docker build --build-arg APP_VERSION=${CIRCLE_SHA1:0:7} -t "$IMAGE" app
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE"
            sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            aws ecr get-login-password --region "$AWS_DEFAULT_REGION" \
              | docker login --username AWS --password-stdin "$REGISTRY"
            docker push "$IMAGE"

  deploy-to-eks:
    parameters:
      namespace:
        type: string
    docker:
      - image: cimg/aws:2024.03
    steps:
      - checkout
      - aws-cli/setup:
          role_arn: ${AWS_ROLE_ARN}
          region: ${AWS_DEFAULT_REGION}
      - run:
          name: 🔧 kubectl → cluster
          command: |
            curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
            sudo install kubectl /usr/local/bin/kubectl
            aws eks update-kubeconfig --region "$AWS_DEFAULT_REGION" --name "$EKS_CLUSTER"
      - run:
          name: 📌 apply and wait for the rollout
          command: |
            IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ECR_REPOSITORY}:${CIRCLE_SHA1}"
            sed -i "s|IMAGE_PLACEHOLDER|${IMAGE}|" k8s/deployment.yaml
            kubectl apply -n << parameters.namespace >> -f k8s/
            kubectl rollout status -n << parameters.namespace >> deployment/hello-courier --timeout=180s

workflows:
  test-build-deploy:
    jobs:
      - test:                            # NO context: tests never see cloud credentials
          matrix:
            parameters:
              node: ["20.18", "22.12", "24.0"]
      - build-and-push:
          context: school-cicd
          requires: [test]
          filters:
            branches:
              only: main                 # only main gets built and pushed
      - deploy-to-eks:
          name: deploy-staging
          namespace: staging
          context: school-cicd
          requires: [build-and-push]
      - hold-for-approval:               # 🛑 the principal's signature: click Approve in the UI
          type: approval
          requires: [deploy-staging]
      - deploy-to-eks:
          name: deploy-production
          namespace: production
          context: school-cicd
          requires: [hold-for-approval]

📄 .gitlab-ci.yml

# 🦊 The SAME pipeline in the GitLab CI dialect (lesson 11).
#   stages: test → build → deploy   (jobs in one stage run in parallel)
# Set these CI/CD variables on the project: AWS_ROLE_ARN, AWS_REGION, AWS_ACCOUNT_ID,
# ECR_REPOSITORY, EKS_CLUSTER. Credentials come from an ID token (OIDC) — no stored keys.
stages: [test, build, deploy]

variables:
  AWS_REGION: ap-south-1

test:
  stage: test
  image: node:${NODE}-alpine
  parallel:
    matrix:                                   # lesson 04: three rulers
      - NODE: ["20", "22", "24"]
  cache:                                      # lesson 04: the drawer, keyed by the file that defines the work
    key:
      files: [app/prepare.js]
    paths: [app/prepared/]
  script:
    - cd app
    - node prepare.js                         # prints "already exists" on a cache hit
    - node --test --test-reporter=spec --test-reporter-destination=stdout
                  --test-reporter=junit --test-reporter-destination=test-results.xml
  artifacts:                                  # lesson 05: the report card — parsed into the MR widget
    when: always
    reports:
      junit: app/test-results.xml
    paths: [app/test-results.xml]
    expire_in: 1 week

.aws-badge: &aws-badge                        # 🪪 OIDC: exchange GitLab's ID token for temporary AWS credentials
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  before_script:
    - >
      export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
      $(aws sts assume-role-with-web-identity
      --role-arn "$AWS_ROLE_ARN" --role-session-name "gitlab-${CI_PIPELINE_ID}"
      --web-identity-token "$GITLAB_OIDC_TOKEN" --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text))

build-and-push:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # only the default branch gets built and pushed
  <<: *aws-badge
  script:
    - apk add --no-cache aws-cli curl
    - REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
    - IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
    - docker build --build-arg APP_VERSION=${CI_COMMIT_SHORT_SHA} -t "$IMAGE" app
    - docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
    - aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
    - docker push "$IMAGE"

.deploy: &deploy
  stage: deploy
  image: amazon/aws-cli:2.17.0
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  <<: *aws-badge
  script:
    - curl -sLO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && install kubectl /usr/local/bin/kubectl
    - aws eks update-kubeconfig --region "$AWS_REGION" --name "$EKS_CLUSTER"
    - IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY}:${CI_COMMIT_SHA}"
    - sed -i "s|IMAGE_PLACEHOLDER|${IMAGE}|" k8s/deployment.yaml
    - kubectl apply -n "$NAMESPACE" -f k8s/
    - kubectl rollout status -n "$NAMESPACE" deployment/hello-courier --timeout=180s

deploy-staging:
  <<: *deploy
  variables: { NAMESPACE: staging }
  environment: { name: staging }

deploy-production:
  <<: *deploy
  needs: [deploy-staging]
  variables: { NAMESPACE: production }
  environment: { name: production }
  when: manual                                # 🛑 the principal's signature: a human presses ▶ in the UI

📄 Jenkinsfile

// 🎩 The SAME pipeline in the Jenkins (declarative) dialect — lesson 11.
//   ✅ test (matrix) → 📦 build & push → 🛑 approve → 🚚 deploy
// Needs on the controller: Docker Pipeline, JUnit and Pipeline: AWS Steps plugins, plus a
// credential `aws-school-cicd` (IAM keys or, better, a role assumed by the agent).
pipeline {
  agent none
  options { timestamps(); disableConcurrentBuilds() }
  environment {
    AWS_REGION     = 'ap-south-1'
    ECR_REPOSITORY = 'hello-courier'
    EKS_CLUSTER    = 'school-eks'
  }
  stages {
    stage('✅ test') {
      matrix {                                         // lesson 04: three rulers, in parallel
        axes { axis { name 'NODE'; values '20', '22', '24' } }
        agent { docker { image "node:${NODE}-alpine" } }
        stages {
          stage('test') {
            steps {
              dir('app') {
                // Jenkins has no built-in cache step: a persistent agent keeps the workspace
                // (so prepared/ survives between builds); ephemeral agents need the Job Cacher plugin.
                sh 'node prepare.js'
                sh '''node --test --test-reporter=spec  --test-reporter-destination=stdout \
                                  --test-reporter=junit --test-reporter-destination=test-results.xml'''
              }
            }
            post { always { junit 'app/test-results.xml' } }   // lesson 05: the report card
          }
        }
      }
    }
    stage('📦 build & push') {
      when { branch 'main' }
      agent any
      steps {
        withAWS(credentials: 'aws-school-cicd', region: env.AWS_REGION) {
          sh '''
            ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
            REGISTRY="${ACCOUNT}.dkr.ecr.${AWS_REGION}.amazonaws.com"
            IMAGE="${REGISTRY}/${ECR_REPOSITORY}:${GIT_COMMIT}"
            docker build --build-arg APP_VERSION=$(echo "$GIT_COMMIT" | cut -c1-7) -t "$IMAGE" app   # POSIX sh: no ${VAR:0:7}
            docker run -d --rm -p 3000:3000 --name smoke "$IMAGE" && sleep 2 \
              && docker exec smoke wget -qO- localhost:3000/healthz && docker stop smoke
            aws ecr get-login-password --region "$AWS_REGION" | docker login --username AWS --password-stdin "$REGISTRY"
            docker push "$IMAGE"
          '''
        }
      }
    }
    stage('🚚 staging') {
      when { branch 'main' }
      agent any
      steps { script { deployTo('staging') } }
    }
    stage('🛑 approve') {
      when { branch 'main' }
      options { timeout(time: 2, unit: 'DAYS') }
      steps { input message: 'Deploy to production?', ok: 'Approve' }   // the principal's signature
    }
    stage('🚚 production') {
      when { branch 'main' }
      agent any
      steps { script { deployTo('production') } }
    }
  }
}

def deployTo(String namespace) {
  withAWS(credentials: 'aws-school-cicd', region: env.AWS_REGION) {
    sh """
      ACCOUNT=\$(aws sts get-caller-identity --query Account --output text)
      IMAGE="\${ACCOUNT}.dkr.ecr.${env.AWS_REGION}.amazonaws.com/${env.ECR_REPOSITORY}:${env.GIT_COMMIT}"
      aws eks update-kubeconfig --region ${env.AWS_REGION} --name ${env.EKS_CLUSTER}
      sed -i "s|IMAGE_PLACEHOLDER|\${IMAGE}|" k8s/deployment.yaml
      kubectl apply -n ${namespace} -f k8s/
      kubectl rollout status -n ${namespace} deployment/hello-courier --timeout=180s
    """
  }
}