CI/CD 스터디 03: Pipeline YAML, Stage, Job, Step 설계

Pipeline YAML에서 workflow, stage, job, step, needs, rules를 구분하고 GitHub Actions, GitLab CI/CD, Jenkins 예제로 설계합니다.

Pipeline YAML은 자동화 순서를 적는 파일이지만, 실제로는 변경 정책을 코드로 표현한 운영 문서입니다. stage, job, step, needs, rules를 구분해야 빠르고 안전한 파이프라인을 만들 수 있습니다.

구성 단위

단위의미설계 기준
workflow / pipeline하나의 자동화 실행 전체언제 실행할지, 어떤 이벤트를 받을지 결정합니다.
stage큰 단계build, test, scan, deploy처럼 사람이 이해하는 흐름입니다.
jobrunner에 예약되는 실행 단위병렬화, 실패 격리, artifact 전달의 기준입니다.
step / scriptjob 안의 명령 단위같은 workspace와 환경을 공유합니다.
needs / dependenciesjob 간 의존성불필요한 stage 대기를 줄이고 필요한 산출물만 받게 합니다.

나쁜 YAML의 전형

처음 파이프라인을 만들 때 가장 흔한 실수는 모든 명령을 하나의 job에 밀어 넣는 것입니다. 이러면 실패 지점이 흐려지고, 캐시와 artifact도 제대로 나누기 어렵습니다.

# 피해야 할 형태: 모든 일을 한 job에서 처리
all-in-one:
  script:
    - npm ci
    - npm run lint
    - npm test
    - npm run build
    - docker build -t app .
    - ./deploy.sh

GitHub Actions 예시

GitHub Actions에서는 workflow event, jobs, steps, needs가 기본 뼈대입니다. 배포 job은 build/test가 모두 통과한 뒤 main 브랜치에서만 실행되도록 분리합니다.

name: app-ci

on:
  pull_request:
  push:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

  build:
    runs-on: ubuntu-latest
    needs: [ test ]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build

  deploy-staging:
    runs-on: ubuntu-latest
    needs: [ build ]
    if: github.ref == 'refs/heads/main'
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy-staging.sh

GitLab CI/CD 예시

GitLab은 stages와 job keyword를 조합합니다. stage 순서만 쓰면 전체 stage가 끝날 때까지 기다리므로, 빠른 feedback이 필요하면 needs를 명시합니다.

stages:
  - test
  - build
  - deploy

lint:
  stage: test
  image: node:22-alpine
  script:
    - npm ci
    - npm run lint

unit-test:
  stage: test
  image: node:22-alpine
  script:
    - npm ci
    - npm test

build-app:
  stage: build
  image: node:22-alpine
  needs: [ lint, unit-test ]
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 7 days

deploy-staging:
  stage: deploy
  needs: [ build-app ]
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - ./scripts/deploy-staging.sh

Jenkins Declarative Pipeline 예시

Jenkinsfile은 Groovy 기반이라 YAML과 다르지만, agent, stages, stage, steps라는 구조를 분명히 나누면 같은 방식으로 읽을 수 있습니다.

pipeline {
  agent any

  stages {
    stage('Test') {
      steps {
        sh 'npm ci'
        sh 'npm run lint'
        sh 'npm test'
      }
    }
    stage('Build') {
      steps {
        sh 'npm run build'
        archiveArtifacts artifacts: 'dist/**', fingerprint: true
      }
    }
    stage('Deploy Staging') {
      when { branch 'main' }
      steps {
        sh './scripts/deploy-staging.sh'
      }
    }
  }
}

설계 원칙

  1. 빠른 실패를 앞에 둡니다. format, lint, unit test가 image build보다 먼저 와야 합니다.
  2. 비싼 작업은 입력 변경이 있을 때만 실행합니다.
  3. 배포 job은 테스트 job과 runner 권한을 분리합니다.
  4. job 이름은 실패 메시지만 봐도 원인을 알 수 있게 씁니다.
  5. YAML이 길어지면 재사용 템플릿을 쓰되, 핵심 deploy 흐름은 숨기지 않습니다.

참고 문서

BGM EVER