본문 바로가기

CI/CD 파이프라인 구축

코드 변경을 검증하고 불변 이미지 digest로 게시한 뒤, 보호된 환경 승인과 제한된 헬스 게이트를 거쳐 동일한 digest를 배포합니다.

지난 절에서는 NestJS 애플리케이션을 Docker 이미지로 만드는 방법을 알아보았습니다.

이번 절에서는 그 이미지를 임의로 다시 만들지 않고, 소스 검증부터 운영 배포까지 하나의 추적 가능한 경로로 연결합니다. 예시는 GitHub Actions와 Docker Hub, 단일 EC2 호스트를 사용하지만 핵심 원칙은 다른 레지스트리와 배포 플랫폼에서도 같습니다.

NestJS CI/CD 품질 게이트와 불변 digest 배포 흐름

SHA 검사·PR 종료/main 게시·불변 digest·보호 승인·제한된 health 확인·유지/명시적 복구의 릴리스 상태이다.

Pull Request와 main 커밋이 설치, lint, test, build 품질 게이트를 통과하고, main만 불변 이미지 digest를 게시한 뒤 보호된 운영 환경에 같은 digest를 배포하며, 제한된 헬스 확인 결과에 따라 새 릴리스를 유지하거나 운영자가 이전 digest를 명시적으로 다시 배포하는 흐름

Nest · Delivery quality gates

검증·게시·승인·복구의 경계

PR과 main의 각 이벤트 커밋에 같은 검증을 적용합니다. main만 이미지를 게시하고 승인 후 같은 digest를 배포합니다. 헬스 실패는 job 실패이며 이전 digest 복구는 운영자의 명시적 재배포입니다.

NestJS CI/CD 품질 게이트 흐름PR 또는 main의 커밋 SHA가 npm ci, lint, test, build를 모두 통과해야 한다. 실패하면 게시가 차단된다. PR은 검증 완료로 끝나고 main은 레지스트리에 이미지 digest를 게시한다. 보호된 production 환경은 같은 digest를 배포한다. 최대 12회의 제한된 헬스 확인이 성공하면 새 릴리스를 유지하고, 실패하면 배포를 실패 처리한 뒤 운영자가 이전에 승인한 digest를 명시적으로 재배포한다.PR / main 이벤트각 이벤트의 SHA품질 게이트ci · linttest · build실패 종료게시·배포 차단main push?이벤트 조건PR 검증 완료운영 secret 없음Registry 게시repository@sha256 digest보호된 운영 배포승인 후같은 digest헬스 통과?최대 12회요청 2s새 릴리스 유지명시적 복구이전 digest 재배포FAILPASSPRMAIN성공실패
NestJS CI/CD 품질 게이트 흐름품질 실패는 종료이며 PASS만 이벤트 판단으로 갑니다. PR은 검증 완료로 끝나고 main만 Registry 게시와 승인 배포로 갑니다. 헬스 성공은 새 릴리스 유지, 실패는 운영자의 명시적 복구입니다. 실패 종료나 PR 완료에서 다음 게시 단계로 이어지는 선은 없습니다.소스 이벤트PR / main SHA모두 통과?ci · linttest · build실패 종료게시·배포 차단main push?이벤트 조건PR 검증 완료운영 secret 없음Registry 게시repository@sha256 digest운영 배포승인 후같은 digest헬스 통과?최대 12회새 릴리스 유지명시적 복구이전 digest 재배포FAILPASSPRMAIN성공실패

01 · verify or stop

소스는 설치·lint·test·build를 모두 통과해야 한다

PR과 main SHA에 같은 검증을 적용합니다. 한 단계라도 실패하면 이미지 게시와 운영 배포는 시작되지 않습니다.

02 · event boundary

PR은 검증에서 끝나고 main만 게시한다

PR job에는 Registry 자격 증명과 운영 secret을 주지 않습니다. 통과한 main push만 이미지를 빌드합니다.

03 · immutable delivery

Registry가 돌려준 digest를 그대로 배포한다

커밋 태그는 추적용 이름입니다. 보호된 production job은 승인 후 정확한 repository@sha256 값을 서버의 두 Compose 명령에 전달합니다.

04 · bounded outcome

제한된 확인 뒤 유지하거나 명시적으로 복구한다

헬스 요청은 2초 타임아웃으로 최대 12회 실행하고 5초씩 기다립니다. 성공하면 유지하고, 실패하면 job을 실패 처리합니다.

자동 롤백은 구현되지 않았습니다. 운영자가 이전에 승인한 digest를 같은 보호 경로로 다시 배포합니다.

파이프라인이 보장하는 것은 “다시 빌드해 비슷한 이미지”가 아니라, 게시한 동일 digest가 승인과 배포 기록을 지나간다는 사실입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • PR 또는 main의 커밋 SHA가 npm ci, lint, test, build를 모두 통과해야 한다. 실패하면 게시가 차단된다. PR은 검증 완료로 끝나고 main은 레지스트리에 이미지 digest를 게시한다. 보호된 production 환경은 같은 digest를 배포한다. 최대 12회의 제한된 헬스 확인이 성공하면 새 릴리스를 유지하고, 실패하면 배포를 실패 처리한 뒤 운영자가 이전에 승인한 digest를 명시적으로 재배포한다.
  • 품질 실패는 종료이며 PASS만 이벤트 판단으로 갑니다. PR은 검증 완료로 끝나고 main만 Registry 게시와 승인 배포로 갑니다. 헬스 성공은 새 릴리스 유지, 실패는 운영자의 명시적 복구입니다. 실패 종료나 PR 완료에서 다음 게시 단계로 이어지는 선은 없습니다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS CI/CD 품질 게이트와 불변 digest 배포 흐름
27요청 2 s
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
커밋 검증

PR 또는 main SHA가 npm ci·lint·test·build를 모두 통과해야 합니다.

대상: 커밋 SHA · 실패: 게시 차단 · PR 성공: 검증에서 끝게시 가능 여부가 정해집니다.
main digest 게시와 배포

main 검증에 성공하면 image digest를 게시하고 protected production이 같은 digest를 배포합니다.

대상: main · 게시: image digest · production: 같은 digest검증한 릴리스가 보호된 환경에 들어갑니다.
제한된 health 확인

최대 12회 확인에 성공하면 릴리스를 유지하고 실패하면 배포를 실패 처리합니다.

확인 상한: 12회 · 성공: 유지 · 실패: 배포 실패새 릴리스의 운영 판정이 나옵니다.
실패 후 명시적 재배포

실패 때 운영자가 이전 승인 digest를 명시적으로 재배포합니다.

복구: 운영자 재배포 · 대상: 이전 승인 digest · 자동 rollback: 원 계약 아님승인된 이전 실행 대상으로 복구를 시도합니다.

NestJS CI/CD 품질 게이트와 불변 digest 배포 흐름

SHA 검사·PR 종료/main 게시·불변 digest·보호 승인·제한된 health 확인·유지/명시적 복구의 릴리스 상태이다.

Pull Request와 main 커밋이 설치, lint, test, build 품질 게이트를 통과하고, main만 불변 이미지 digest를 게시한 뒤 보호된 운영 환경에 같은 digest를 배포하며, 제한된 헬스 확인 결과에 따라 새 릴리스를 유지하거나 운영자가 이전 digest를 명시적으로 다시 배포하는 흐름

Nest · Delivery quality gates

검증·게시·승인·복구의 경계

PR과 main의 각 이벤트 커밋에 같은 검증을 적용합니다. main만 이미지를 게시하고 승인 후 같은 digest를 배포합니다. 헬스 실패는 job 실패이며 이전 digest 복구는 운영자의 명시적 재배포입니다.

NestJS CI/CD 품질 게이트 흐름PR 또는 main의 커밋 SHA가 npm ci, lint, test, build를 모두 통과해야 한다. 실패하면 게시가 차단된다. PR은 검증 완료로 끝나고 main은 레지스트리에 이미지 digest를 게시한다. 보호된 production 환경은 같은 digest를 배포한다. 최대 12회의 제한된 헬스 확인이 성공하면 새 릴리스를 유지하고, 실패하면 배포를 실패 처리한 뒤 운영자가 이전에 승인한 digest를 명시적으로 재배포한다.PR / main 이벤트각 이벤트의 SHA품질 게이트ci · linttest · build실패 종료게시·배포 차단main push?이벤트 조건PR 검증 완료운영 secret 없음Registry 게시repository@sha256 digest보호된 운영 배포승인 후같은 digest헬스 통과?최대 12회요청 2s새 릴리스 유지명시적 복구이전 digest 재배포FAILPASSPRMAIN성공실패
NestJS CI/CD 품질 게이트 흐름품질 실패는 종료이며 PASS만 이벤트 판단으로 갑니다. PR은 검증 완료로 끝나고 main만 Registry 게시와 승인 배포로 갑니다. 헬스 성공은 새 릴리스 유지, 실패는 운영자의 명시적 복구입니다. 실패 종료나 PR 완료에서 다음 게시 단계로 이어지는 선은 없습니다.소스 이벤트PR / main SHA모두 통과?ci · linttest · build실패 종료게시·배포 차단main push?이벤트 조건PR 검증 완료운영 secret 없음Registry 게시repository@sha256 digest운영 배포승인 후같은 digest헬스 통과?최대 12회새 릴리스 유지명시적 복구이전 digest 재배포FAILPASSPRMAIN성공실패

01 · verify or stop

소스는 설치·lint·test·build를 모두 통과해야 한다

PR과 main SHA에 같은 검증을 적용합니다. 한 단계라도 실패하면 이미지 게시와 운영 배포는 시작되지 않습니다.

02 · event boundary

PR은 검증에서 끝나고 main만 게시한다

PR job에는 Registry 자격 증명과 운영 secret을 주지 않습니다. 통과한 main push만 이미지를 빌드합니다.

03 · immutable delivery

Registry가 돌려준 digest를 그대로 배포한다

커밋 태그는 추적용 이름입니다. 보호된 production job은 승인 후 정확한 repository@sha256 값을 서버의 두 Compose 명령에 전달합니다.

04 · bounded outcome

제한된 확인 뒤 유지하거나 명시적으로 복구한다

헬스 요청은 2초 타임아웃으로 최대 12회 실행하고 5초씩 기다립니다. 성공하면 유지하고, 실패하면 job을 실패 처리합니다.

자동 롤백은 구현되지 않았습니다. 운영자가 이전에 승인한 digest를 같은 보호 경로로 다시 배포합니다.

파이프라인이 보장하는 것은 “다시 빌드해 비슷한 이미지”가 아니라, 게시한 동일 digest가 승인과 배포 기록을 지나간다는 사실입니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • PR 또는 main의 커밋 SHA가 npm ci, lint, test, build를 모두 통과해야 한다. 실패하면 게시가 차단된다. PR은 검증 완료로 끝나고 main은 레지스트리에 이미지 digest를 게시한다. 보호된 production 환경은 같은 digest를 배포한다. 최대 12회의 제한된 헬스 확인이 성공하면 새 릴리스를 유지하고, 실패하면 배포를 실패 처리한 뒤 운영자가 이전에 승인한 digest를 명시적으로 재배포한다.
  • 품질 실패는 종료이며 PASS만 이벤트 판단으로 갑니다. PR은 검증 완료로 끝나고 main만 Registry 게시와 승인 배포로 갑니다. 헬스 성공은 새 릴리스 유지, 실패는 운영자의 명시적 복구입니다. 실패 종료나 PR 완료에서 다음 게시 단계로 이어지는 선은 없습니다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1NestJS CI/CD 품질 게이트와 불변 digest 배포 흐름
27요청 2 s
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
커밋 검증

PR 또는 main SHA가 npm ci·lint·test·build를 모두 통과해야 합니다.

대상: 커밋 SHA · 실패: 게시 차단 · PR 성공: 검증에서 끝게시 가능 여부가 정해집니다.
main digest 게시와 배포

main 검증에 성공하면 image digest를 게시하고 protected production이 같은 digest를 배포합니다.

대상: main · 게시: image digest · production: 같은 digest검증한 릴리스가 보호된 환경에 들어갑니다.
제한된 health 확인

최대 12회 확인에 성공하면 릴리스를 유지하고 실패하면 배포를 실패 처리합니다.

확인 상한: 12회 · 성공: 유지 · 실패: 배포 실패새 릴리스의 운영 판정이 나옵니다.
실패 후 명시적 재배포

실패 때 운영자가 이전 승인 digest를 명시적으로 재배포합니다.

복구: 운영자 재배포 · 대상: 이전 승인 digest · 자동 rollback: 원 계약 아님승인된 이전 실행 대상으로 복구를 시도합니다.

NestJS에서 CI/CD란 무엇인가?

CI/CD는 코드를 자주 합치고, 기계적인 품질 검사를 반복하며, 검증 결과와 배포 기록을 연결하는 개발 관행입니다.

지속적 통합(Continuous Integration)

Pull Request와 main push는 같은 검증 job에서 다음 단계를 순서대로 통과합니다.

  • npm ci: lockfile과 일치하는 의존성 설치
  • npm run lint -- --no-fix: CI에서 소스를 고치지 않는 정적 검사
  • npm run test -- --runInBand: 테스트 실행
  • npm run build: TypeScript 컴파일과 NestJS 빌드 확인

이 절의 테스트 명령은 8장의 Jest 설정을 사용하는 예제 프로젝트를 전제로 합니다. npm run test -- --runInBand는 Jest 옵션이며, 현재 기본 Nest starter의 Vitest 프로젝트에는 그대로 적용하지 않습니다. 다른 runner는 해당 프로젝트의 script와 CI 옵션으로 맞춥니다.

하나라도 실패하면 이후 이미지 게시와 배포는 시작되지 않습니다. PR은 이 검증 결과까지만 얻으며 레지스트리 자격 증명이나 운영 비밀에 접근하지 않습니다.

지속적 제공(Continuous Delivery)

검증을 통과한 main 커밋은 컨테이너 이미지를 한 번 빌드해 레지스트리에 게시합니다. 태그는 커밋을 찾기 위한 이름이고, 실제 배포 입력은 게시 결과가 돌려준 sha256:... digest입니다.

운영 job이 GitHub의 보호된 production environment 승인을 기다린다면 이 파이프라인은 지속적 제공입니다. 배포 가능한 산출물은 자동으로 준비하지만 운영 반영에는 명시적인 승인이 남아 있기 때문입니다.

지속적 배포(Continuous Deployment)

승인 절차까지 자동 정책으로 대체하고, 모든 검증을 통과한 변경을 사람의 개입 없이 운영에 반영할 때 지속적 배포라고 부릅니다. 이 절의 예시는 보호된 환경 승인을 사용하므로 지속적 배포라고 주장하지 않습니다.


품질 게이트와 배포 불변성

파이프라인의 중요한 경계는 다음과 같습니다.

소스 이벤트: Pull Request와 main push의 각 이벤트 커밋을 같은 verify job으로 검증합니다. 기본 checkout에서 PR은 merge ref의 커밋을, main push는 갱신된 브랜치의 커밋을 사용하므로 둘의 SHA가 같다고 가정하지 않습니다.

품질 검증: 설치, lint, test, build가 모두 성공해야 다음 job이 실행됩니다.

이미지 게시: main push만 이미지를 빌드하고 Registry가 기록한 불변 digest를 출력합니다.

보호된 배포: 승인된 production environment job이 이미지 태그가 아니라 정확한 digest를 서버에 전달합니다.

제한된 헬스 게이트: 정해진 횟수와 타임아웃 안에서 상태를 확인합니다. 성공하면 새 릴리스를 유지하고, 실패하면 job을 실패 처리합니다.

명시적 복구: 이 예시는 자동 롤백을 구현하지 않습니다. 실패 시 운영자가 배포 기록에서 이전에 승인한 digest를 선택해 같은 보호 경로로 다시 배포합니다.

latest 같은 가변 태그를 운영 배포 입력으로 사용하면 승인한 이미지와 실제로 내려받는 이미지가 달라질 수 있습니다. 이 절에서는 이미지 게시 job의 digest 출력을 배포 job까지 그대로 전달해 그 우회를 막습니다.


실행 전 준비

  • GitHub 저장소에 NestJS 프로젝트, package-lock.json, Dockerfile, .dockerignore, compose.yml이 있어야 합니다.
  • compose.yml의 app.image는 ${IMAGE:?IMAGE digest is required}처럼 외부 IMAGE 값을 참조해야 합니다.
  • EC2 호스트의 /opt/nestjs-app에는 운영용 compose.yml과 서버에서 관리하는 설정이 있어야 합니다.
  • EC2 호스트에는 현재 docker compose 명령과 curl이 설치되어 있어야 합니다.
  • Docker Hub에는 비밀번호 대신 권한을 제한한 액세스 토큰을 사용합니다.
  • SSH 호스트 키는 신뢰할 수 있는 경로에서 미리 확인하고 known_hosts 한 줄 형식으로 보관합니다. 실행 중 ssh-keyscan으로 즉석 신뢰하지 않습니다.

앞 절의 개발용 compose.yaml은 로컬 소스로 이미지를 빌드합니다. 여기서 서버가 사용하는 운영용 compose.yml은 게시된 이미지를 실행하는 별도 설정입니다. app.image에 위 IMAGE 참조를 두고, 운영 환경 변수·포트·네트워크·볼륨을 준비합니다. 파일 이름을 바꾸는 것만으로 build 설정이 불변 이미지 배포 설정으로 바뀌지는 않습니다.

아래 헬스 게이트는 GET /health가 준비 상태를 검사하여 정상일 때 2xx, 준비되지 않았을 때 실패 상태를 반환하는 계약을 전제로 합니다. Nest가 이 경로를 자동 생성하지는 않습니다. 다음 절의 세 probe를 구현한 누적 프로젝트에서는 이 경로를 readiness와 연결하거나, 별도 변형에서 요청 경로를 실제 /health/ready 계약으로 맞춥니다. 전역 접두사·URI 버전을 사용했다면 probe의 실제 URL도 확인합니다. production의 required reviewers를 실제로 설정해야 아래 job이 사람의 승인을 기다립니다.

현재 예시는 프로젝트 런타임으로 Node.js 24 LTS를 사용합니다. actions/checkout@v7과 actions/setup-node@v6도 Node 24 기반의 현재 메이저이며, Docker 공식 액션은 docker/login-action@v4, docker/build-push-action@v7을 사용합니다.


GitHub Actions 워크플로우

.github/workflows/main.yml에 다음 전체 워크플로우를 작성합니다. PR 검증과 main 이미지 게시, 보호된 운영 배포를 분리하며, 아래의 Builder 준비 조각은 해당 이미지 job에 추가하는 별도 보완입니다.

.github/workflows/main.yml
name: NestJS quality gate and deploy

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

env:
  NODE_VERSION: '24'
  IMAGE_NAME: nestjs-app

permissions:
  contents: read

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          persist-credentials: false

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm

      - name: Install locked dependencies
        run: npm ci

      - name: Lint without rewriting source
        run: npm run lint -- --no-fix

      - name: Test
        run: npm run test -- --runInBand

      - name: Build NestJS
        run: npm run build

  image:
    needs: verify
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    outputs:
      repository: ${{ steps.image-meta.outputs.repository }}
      digest: ${{ steps.image-build.outputs.digest }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          persist-credentials: false

      - name: Define image repository
        id: image-meta
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        run: echo "repository=docker.io/${DOCKER_USERNAME}/${IMAGE_NAME}" >> "$GITHUB_OUTPUT"

      - name: Log in to Docker Hub
        uses: docker/login-action@v4
        with:
          registry: docker.io
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Build and push once
        id: image-build
        uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.image-meta.outputs.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: image
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    environment:
      name: production
    concurrency:
      group: production
      cancel-in-progress: false
    steps:
      - name: Deploy exact digest and run bounded health gate
        env:
          EC2_HOST: ${{ secrets.EC2_HOST }}
          EC2_USERNAME: ${{ secrets.EC2_USERNAME }}
          EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }}
          EC2_SSH_KNOWN_HOSTS: ${{ secrets.EC2_SSH_KNOWN_HOSTS }}
          IMAGE_REF: ${{ needs.image.outputs.repository }}@${{ needs.image.outputs.digest }}
        run: |
          install -d -m 700 "$HOME/.ssh"
          printf '%s\n' "$EC2_SSH_KEY" > "$HOME/.ssh/id_ed25519"
          chmod 600 "$HOME/.ssh/id_ed25519"
          printf '%s\n' "$EC2_SSH_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
          chmod 600 "$HOME/.ssh/known_hosts"

          ssh \
            -i "$HOME/.ssh/id_ed25519" \
            -o BatchMode=yes \
            -o StrictHostKeyChecking=yes \
            -o ConnectTimeout=10 \
            -o ServerAliveInterval=15 \
            -o ServerAliveCountMax=2 \
            "${EC2_USERNAME}@${EC2_HOST}" \
            bash -s -- "$IMAGE_REF" <<'REMOTE'
          set -euo pipefail

          IMAGE_REF="$1"
          cd /opt/nestjs-app

          sudo env IMAGE="$IMAGE_REF" docker compose pull app
          sudo env IMAGE="$IMAGE_REF" docker compose up -d --no-deps app

          healthy=false
          for attempt in $(seq 1 12); do
            if curl --fail --silent --show-error --max-time 2 \
              http://127.0.0.1:3000/health > /dev/null; then
              healthy=true
              break
            fi
            sleep 5
          done

          if [ "$healthy" != "true" ]; then
            echo "Health gate failed; release is not accepted."
            echo "Rollback is explicit: redeploy a previously approved digest."
            exit 1
          fi
          REMOTE

워크플로우를 읽는 법

verify는 PR과 main에서 모두 실행됩니다. 반면 image는 main push 조건을 다시 확인하므로 PR에서는 Docker Hub 비밀이 열리지 않고 이미지도 게시되지 않습니다.

docker/build-push-action의 digest 출력은 Registry에 게시된 manifest를 가리킵니다. 배포 job은 repository@digest를 IMAGE_REF로 만들고, 서버의 docker compose pull과 up에 같은 값을 전달합니다. 두 명령 모두 sudo env IMAGE="$IMAGE_REF" ... 형태이므로 sudo가 기본 환경을 정리하더라도 필요한 이미지 값이 명시적으로 전달됩니다.

type=gha 캐시는 빌드 성공 조건과 별도로 Builder의 지원 조건을 확인합니다. Docker의 캐시 문서에 따르면 기본 docker 드라이버에서는 containerd image store가 켜져 있어야 합니다. 이 조건을 충족하지 않는 환경은 공식 Buildx 설정 예시처럼 캐시를 지원하는 Builder를 먼저 구성합니다. 원 워크플로우의 빌드 단계는 유지하되, image.steps에서 Build and push once 전에 다음 추가 단계를 둘 수 있습니다.

image job에 추가할 Builder 준비 단계
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v4
  with:
    driver: docker-container

배포는 서드파티 SSH action 대신 GitHub 호스팅 러너의 OpenSSH 클라이언트를 직접 사용합니다. 서드파티 action을 사용해야 한다면 변경 가능한 태그가 아니라 검토한 전체 commit SHA에 고정해야 합니다.

헬스 확인은 요청당 2초 타임아웃, 최대 12회, 재시도 사이 5초 대기로 제한됩니다. 모두 실패하면 job과 GitHub deployment 기록이 실패하지만, 위 스크립트는 이전 컨테이너로 자동 복귀하지 않습니다. 자동 롤백이라고 설명하려면 이전 digest 저장, 전환, 재검증까지 별도로 구현해야 합니다.

실행 결과를 단계별로 판단하기 PR에서 테스트가 실패하면 verify가 실패하고 이미지 게시·배포는 실행되지 않습니다. main 검증과 이미지 게시가 성공하여 digest A를 얻었더라도 운영 승인 대기 중에는 아직 A를 서버에 배포하지 않습니다. 승인 뒤 A를 실행하고 처음 두 헬스 요청이 실패하다 세 번째가 성공하면 세 번째 요청에서 반복을 끝내고 A를 유지합니다. 두 번의 실패 뒤에만 각각 5초 대기가 발생합니다.

독립 점검 승인된 새 digest B를 실행한 뒤 12번의 헬스 요청이 모두 실패했습니다. 이전 정상 digest는 A로 기록되어 있습니다. job 결과, 서버가 자동으로 A로 돌아가는지, 운영자가 다음에 확인하고 선택할 입력을 설명하세요. 요청·대기만의 최대 시간을 계산할 때는 원 코드가 마지막 실패 뒤에도 sleep 5를 실행한다는 점을 포함합니다. Compose·SSH·명령 실행 부대 시간은 이 계산에서 제외합니다.

실패 기록과 실제 복구를 구분하기

job은 실패하며 B의 배포를 정상 릴리스로 수용하지 않습니다. 코드에 A로 다시 전환하는 명령이 없으므로 실패 기록만으로 서버가 자동 복귀하지 않습니다. 운영자는 헬스·앱 로그와 사용자 영향을 확인하고, 기록된 repository@A의 digest를 같은 보호 경로로 재배포할지 판단한 뒤 다시 헬스와 외부 관측을 검증합니다. 요청 타임아웃은 최대 12 × 2 = 24초, 실패 뒤 대기는 12 × 5 = 60초로 이 두 부분은 최대 84초입니다. 전체 배포 시간이나 복구 시간의 보장은 아닙니다.


비밀과 승인 경계 설정

Docker Hub 자격 증명은 저장소 Actions secret으로 설정하되 main 전용 image job에서만 참조합니다.

  • DOCKER_USERNAME: Docker Hub 사용자 이름
  • DOCKERHUB_TOKEN: 이미지 push 범위만 가진 액세스 토큰

EC2 접속 값은 저장소 공용 secret이 아니라 production environment secret으로 설정합니다.

  • EC2_HOST: 운영 호스트 주소
  • EC2_USERNAME: 제한된 운영 사용자
  • EC2_SSH_KEY: 해당 사용자용 SSH 개인 키
  • EC2_SSH_KNOWN_HOSTS: 사전에 확인한 호스트 공개 키의 known_hosts 항목

production environment에는 필요한 경우 required reviewers, self-review 방지, main만 허용하는 deployment branch 규칙을 설정하고 관리자 우회도 허용하지 않습니다. 승인 대기 중인 job은 environment secret에 접근할 수 없으므로 승인 전에 운영 비밀이 열리지 않는 경계가 만들어집니다. 사용할 수 있는 보호 규칙은 GitHub 요금제와 저장소 공개 범위에 따라 다를 수 있습니다.

운영 DB 비밀번호와 JWT 비밀값은 이미지나 GitHub workflow에 넣지 않습니다. 서버의 제한된 env 파일, AWS Secrets Manager, SSM Parameter Store, Vault 같은 운영 비밀 저장소에서 런타임에 주입합니다.

Pull Request 검증과 main 이미지 게시를 분리하고, Registry의 불변 digest가 보호된 production 환경 승인 뒤에만 운영 비밀과 만나 동일한 digest로 배포되며, GitHub Actions 기록과 런타임 관측으로 결과를 추적하는 Secure paved road 아키텍처. PR의 운영 직접 배포와 가변 태그 배포는 보호 경계 전에 차단된다.

Nest · Protected deployment

승인과 digest가 배포를 통제한다

PR 검증·main 게시·운영 배포를 서로 다른 권한 경계로 둡니다. production 보호 규칙과 승인자는 별도로 설정해야 하며 승인 뒤에만 운영 비밀과 동일 digest를 사용하는 배포 job이 실행됩니다.

GitHub Actions 보호 배포 아키텍처Repository Change, GitHub Actions and Registry, Protected Production 세 영역이 있다. Pull Request와 main push는 먼저 CI verifier를 지난다. PR은 검증으로 끝나며 main만 image publisher를 거쳐 Registry에 불변 digest를 게시한다. 그 digest는 production environment 승인 뒤 운영 비밀과 만나 동일한 digest로 runtime에 배포된다. workflow와 deployment 기록, 런타임 헬스와 로그는 audit and observability로 모인다. PR의 운영 직접 배포와 latest 같은 가변 태그 배포는 production 경계 전에 차단된다.Pull Request운영 secret 없음main push게시 후보 SHA동일 품질 검증ci · lint · test/build실패하면 종료main만 게시Registry token한 번 build/push불변 Registryrepository@sha256 digestlatest 치환 금지production 보호reviewer · main 제한승인 후 EC2 secret운영 실행같은 digestpull/up · health기록·관측job · deploymenthealth · logsPR 검증 완료운영 배포 없음MAINPR
GitHub Actions 보호 배포 아키텍처PR과 main은 별개 입력으로 동일 검증에 합쳐집니다. PR은 검증 완료로 끝나고 main만 게시합니다. Registry digest가 production 보호를 거쳐 같은 값으로 배포됩니다. 검증 기록과 runtime 신호가 각각 기록·관측에 들어갑니다. PR 직접 운영 배포와 latest 치환은 허용되지 않는 정책이며 보충 설명에 남깁니다.Pull Request운영 secret 없음main push게시 후보 SHA동일 품질 검증ci · linttest / buildPR 검증 완료운영 배포 없음main만 게시Registry tokenbuild / push불변 Registryrepository@sha256 digestlatest 치환 금지production 보호reviewer · main 제한승인 후 EC2 secret운영 실행같은 digestpull / up · health기록·관측job · deploymenthealth · logsPRMAIN

01 · pull request

PR은 같은 품질 게이트를 통과하고 검증에서 멈춘다

npm ci, lint, test, build 결과와 로그만 남깁니다. Registry token과 운영 environment secret은 열리지 않습니다.

02 · main publish

main만 이미지를 한 번 게시하고 digest를 남긴다

커밋 태그는 추적용이고, 다음 경계로 전달되는 값은 Registry가 반환한 불변 repository@sha256입니다.

03 · approval before secret

보호된 production 승인이 먼저다

required reviewer, self-review 방지, main 제한과 관리자 우회 금지 정책을 적용합니다. 승인된 job만 EC2 environment secret에 접근합니다.

04 · exact deploy / evidence

같은 digest를 배포하고 기록과 운영 신호를 연결한다

Compose는 명시적으로 전달받은 digest를 pull·up합니다. GitHub job과 deployment 기록, 제한된 헬스 결과, 외부 로그·지표가 감사와 관측 근거가 됩니다.

05 · forbidden bypass

PR 직접 배포와 가변 태그 배포는 경계 전에 중단한다

latest나 승인되지 않은 경로는 소스, digest, 승인, 배포 사이의 연결을 끊습니다.

헬스 실패는 자동 롤백이 아닙니다. 이전에 승인한 digest의 수동 재배포도 같은 보호 경로를 거칩니다.

  • 승인과 비밀 경계
  • 기록·정책 경로
  • 금지된 우회

권한은 job별로 작게 열고, 운영 비밀은 environment 승인 뒤에만 열며, 배포 값은 repository@digest로 끝까지 고정합니다.


감사, 관측, 복구

GitHub Actions 로그와 environment deployment 기록은 누가 어떤 커밋에서 어떤 digest를 게시하고 승인했는지 추적하는 출발점입니다. 애플리케이션 로그, 지표, 경보와 지속적인 가용성 관측은 별도의 운영 시스템이 담당해야 합니다. 배포 직후의 제한된 헬스 게이트만으로 이후 가용성을 보장할 수는 없습니다.

복구 절차는 다음처럼 명시적으로 운영합니다.

  1. 실패한 deployment와 헬스 로그를 확인합니다.
  2. 이전에 승인되어 정상 동작한 repository@sha256:... 값을 배포 기록에서 선택합니다.
  3. 동일한 production 승인 경로로 그 digest를 다시 배포합니다.
  4. 제한된 헬스 게이트와 외부 모니터링을 다시 확인합니다.

PR에서 운영 job을 직접 호출하는 경로, 승인되지 않은 job이 environment secret을 읽는 경로, latest 태그로 digest를 바꾸는 경로는 허용하지 않습니다. 편의를 위한 우회 하나가 소스·산출물·승인·배포 기록의 연결을 끊기 때문입니다.


CI/CD 파이프라인의 목적은 단계 수를 늘리는 것이 아니라, 실패해야 할 지점에서 빠르게 멈추고 승인한 동일 산출물만 다음 경계로 이동시키는 것입니다.

NestJS 프로젝트에서는 npm ci → lint → test → build를 먼저 고정하고, 그 위에 불변 이미지 digest, 보호된 운영 승인, 제한된 헬스 확인, 명시적 복구와 관측을 한 층씩 더하면 됩니다.