매번 코드를 수정하고 빌드 테스트를 하고,
직접 서버에서 명령어를 치면서 배포를 하는데 직접 하다보니 매번 배포 절차도 동일하지 않고,,
코드를 수정해서 깃헙에 푸시한 다음부터는 자동으로 서비스 배포가 이루어지도록 CI/CD 파이프라인을 구축했다.
CI/CD는 코드 변경 이후의 과정을 자동화하기 위한 개발 방식이다.
CI (Continuous Integration, 지속적 통합)
개발자가 코드를 GitHub에 푸시하면 자동으로 빌드와 테스트를 수행해 변경된 코드가 문제없이 동작하는지 검증하는 단계다.
이를 통해 배포 전에 오류를 사전에 발견할 수 있다.
문제가 없다면 main 브랜치에 합병하게끔 코드를 통합해서 관리를 할 수 있게끔 한다.
CD(Continuous Deployment 또는 Continuous Delivery)
CI 과정을 통과한 코드를 실제 서버에 자동으로 배포하는 단계다.
기존처럼 서버에 접속해 수동으로 명령어를 실행할 필요 없이, 일관된 방식으로 빠르게 배포를 진행할 수 있다.
Continuous Delivery (지속적 전달)
- 배포 가능한 상태까지 자동으로 만들어줌
- 실제 운영 배포는 사람이 진행
- CI 보다 한단계 더 나아간 것 이라고 보면 된다.
Continuous Deployment (지속적 배포)
- CI를 통과하면 자동으로 운영 서버까지 배포
- 사람이 개입하지 않음
- CI + Delivery 보다 한단계 더 나아간 것이라고 보면 쉽다.
CI/CD를 구현하는 방법은 다양하다.
대표적으로는 Jenkins, GitLab CI/CD, CircleCI, Travis CI, GitHub Actions와 같은 도구들이 있다.
각 도구는 파이프라인을 구성하고, 빌드·테스트·배포 과정을 자동화한다는 공통된 목적을 가지고 있지만, 설치 방식이나 사용 편의성, 그리고 프로젝트와의 연동 방식에서 차이가 있다.
간단하게, Jenkins는 높은 확장성과 유연성을 제공하지만, 직접 서버를 구축하고 관리해야 하는 부담이 있다.
GitLab CI/CD는 GitLab과 강하게 통합되어 있어 일관된 개발 환경을 구성하기 좋다.
GitHub Actions
이 중에서 GitHub Actions를 선택한 이유는, 프로젝트가 GitHub을 통해 관리되고 있었기 때문이다.
별도의 CI/CD 도구를 추가로 도입하지 않아도, GitHub 저장소와 자연스럽게 연동되어 워크플로우를 구성할 수 있었다.
또한 .github/workflows 디렉토리 내에 YAML 파일만 정의하면 바로 파이프라인을 실행할 수 있어,
별도의 서버 구축 없이도 빠르게 CI/CD 환경을 구성할 수 있다는 점이 큰 장점이었다.
절차
코드 작성 + 새 브랜치로 푸시 - PR 요청 - 검증 - main 브랜치 병합 - 운영 배포
여기서 PR 요청 단계와 main 브랜치에 병합 되는 순간 워크플로우를 통한 자동 검증 + 배포가 들어가게 된다.
https://docs.github.com/en/actions/get-started/quickstart
Quickstart for GitHub Actions - GitHub Docs
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that run tests whenever you push a change to your repository, or that deploy
docs.github.com
github에서 가이드를 제시해주고 있다. 한번 따라해보자.
1. workflow 생성
github actions를 이용하기 위해서는 .github/workflows 내에 yaml파일이 있어야한다.
이 파일 내에 내가 자동으로 진행할 단계들을 작성하는 것이다.


2. 파일 저장
파일을 새 브랜치를 파서 저장한다.



워크 플로우의 키워드
워크플로우를 작성할 때 아래 키워드들을 이용해서 단계를 작성해줘야 한다.
- jobs : 전체 작업 단위 (실행할 여러 job들의 모음)
- on : 워크플로우가 실행되는 조건 (push, pull_request 등 이벤트)
- runs-on : 작업이 실행될 환경 (예: ubuntu-latest - 가장 많이 쓰이는 최신 우분투 리눅스 서버이다. 이외에 macos, window도 가능)
- steps : job 안에서 실제로 실행되는 작업들의 순서
- name : 해당 step의 이름
- uses : 이미 만들어진 Action을 가져와 실행하는 것 (예: 코드 체크아웃, Node 설치 등)
- run : 쉘 명령어를 직접 실행하는 것 (예: npm install, build, 배포 스크립트 실행)
- with : uses로 실행하는 Action에 옵션이나 값을 전달할 때 사용
- env : 환경 변수 (API 키, 실행 환경 값 등)
- needs: 어떤 job의 실행이 먼저 되어야 할 경우 작성
환경 변수
워크플로우 파일은 깃헙에 올라가기 때문에 환경변수(포트번호, SSH비밀번호 등)은 직접 작성되면 안된다.
흔히 쓰는 proceess.env.{변수명} 처럼 ${{ secrets.SERVER_HOST }} 이런식으로 secrets.{변수명} 으로 작성한다.
변수를 작성하는 공간은 GitHub Secrets에 등록한다.
1. Settings > Secrets and variables > Actions > New repository secret 클릭


NestJS + Docker 배포
이제 NestJS를 Docker 환경에서 자동으로 배포하기 위한 파이프라인을 구성해본다.
Docker를 활용한 배포 방식은 크게 두 가지로 나눌 수 있을 것이다.
1. 가상 서버에서 이미지 빌드
- 1) GitHub Actions의 가상 서버에서 Docker 이미지를 빌드
- 2) 빌드된 이미지를 Docker Hub에 업로드
- 3) 실제 배포 서버에서는 해당 이미지를 pull 받아 컨테이너 실행
장점
- 실제 서버에서 빌드 과정이 없기 때문에 배포 속도가 빠르다.
- 빌드된 이미지를 그대로 사용하므로 모든 서버에서 동일한 환경을 유지할 수 있다.
- CI 단계에서 빌드 검증까지 함께 수행할 수 있다.
단점
- GitHub Actions 러너의 리소스를 사용하므로 비용에 영향을 받을 수 있다
2. 실제 서버에서 이미지 빌드
- 1) GitHub Actions에서 배포 서버로 프로젝트 코드를 전송
- 2) 배포 서버에서 Docker 이미지를 직접 빌드
- 3) 컨테이너 실행
장점
- CI 환경의 리소스를 거의 사용하지 않으므로 비용 부담이 적다.
- 단순한 구조로 빠르게 구성할 수 있다.
단점
- 서버마다 개별적으로 빌드가 수행되므로 환경 차이가 발생할 수 있다.
- 빌드 과정에서 서버 리소스를 사용하기 때문에 서비스에 영향을 줄 수 있다.
- 배포 시간이 상대적으로 길어질 수 있다.
1번 방법이 CI를 최대한 활용할 수 있는 방법일 것이다.
하지만 나는 비용 부담을 줄이고, 단일 서버를 운영중이므로 구조가 단순한 2번 방법으로 진행을 했다.
빌드 테스트 및 서버 접속 확인
일단 NestJS 빌드 테스트가 되는지, 실제 서버로 SSH 접속이 되고 있는지 확인을 시도 했다.
SSH 접속에 대한 환경변수는 따로 Secrects로 등록했다.
SSH에 접속을 성공 했으면, echo 명령어로 로그를 찍을 수 있게 했다.
deploy 단계에서 push이벤트가 일어날때 브랜치가 main일 때만 일어나도로고 if 키워드를 사용했다.
PR 단계에서 deploy가 일어나는 것을 방지하기 위함이다.
# 1. 워크플로우의 이름 (GitHub Actions 탭에 표시됨)
name: NestJS Continuous Integration
# 2. 언제 이 자동화를 실행할지 정의 (트리거)
on:
push:
branches: [ "main" ] # main 브랜치에 코드가 푸시될 때
pull_request:
branches: [ "main" ] # main 브랜치로 합쳐달라는 요청(PR)이 올 때
# paths-ignore: # 특정 파일 수정 시에는 실행하지 않을 경우 명시
# 3. 실행할 작업(Job)들의 모음
# 각 Job은 독립적인 가상 서버 (Runner)에서 실행됨
# 기본적으로 각 Job들은 병렬로 실행됨
# needs: 작업간 순서를 정할 수 있음 ex) 빌드 및 테스트 -> 배포
jobs:
# [1단계] 빌드 및 테스트 단계: 'build-and-test'라는 이름의 작업을 정의
build-and-test:
# 실행 환경: GitHub에서 제공하는 최신 우분투 리눅스 서버 사용. 가장 빠르고 저렴
runs-on: ubuntu-latest
# 작업 내부의 상세 단계(Step)
# 순차적인 명령 집합
steps:
# (1) 가상 서버로 내 레포지토리 코드를 내려받음 (체크아웃)
# uses: 깃헙(actions)에서 만든 액션 레포지토리
# 내 현재 레포지토리 코드를 가상 서버로 옮겨줌
# owner/repo@version 형식
- name: Checkout source code
uses: actions/checkout@v4
# (2) Node.js 환경 설치 (NestJS 실행에 필요)
# uses: 깃헙(actions)에서 만든 액션 레포지토리 setup-node의 v4를 가져다 쓴다는 것.
# 가상서버에 node 환경을 만들어 줌
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0' # 프로젝트에서 사용하는 노드 버전 지정
cache: 'npm' # npm 캐시를 사용하여 다음 실행 시 속도 향상
# (3) 의존성 라이브러리 설치
# 'npm install' 대신 'npm ci'를 권장 (package-lock.json에 명시된 버전 엄격 준수)
- name: Install dependencies
run: npm ci
# (4) 코드 스타일 검사 (Lint)
# 협업 시 일관된 코드 규칙을 지켰는지 확인
#- name: Run Lint
# run: npm run lint
# (5) 테스트 코드 실행 (Unit Test)
# API의 기능들이 정상적으로 작동하는지 확인
#- name: Run Tests
# run: npm run test
# (6) NestJS 빌드 테스트
# TypeScript 코드가 JavaScript로 에러 없이 컴파일되는지 최종 확인
- name: Build project
run: npm run build
# (7) 결과 알림 (성공 시 메시지 출력)
- name: Success Message
if: success()
run: echo "🚀 NestJS CI Pipeline Passed Successfully!"
# [2단계] 배포 (조건부 실행)
# SSH를 이용하여 배포 서버 연결
deploy:
# 조건 1: 앞선 'build-and-test'가 성공해야 함
needs: build-and-test
# 조건 2: '이벤트'가 pull_request가 아니고, 브랜치가 main일 때만 실행
# 즉, PR 단계에서는 deploy를 하지 않음
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: SSH Connection Test
uses: appleboy/ssh-action@v1.0.3
with: # Github Secrets에 환경변수 등록
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
# key: ${{ secrets.SERVER_KEY }} # .pem 키파일을 사용할 경우
password: ${{ secrets.SERVER_PASSWORD }} # 패스워드를 이용하는 경우
# port: 22 # 포트 기본값 22
script: |
echo "✅ 서버 접속에 성공했습니다!"
echo "현재 서버 시간: $(date)"
echo "도커 설치 여부: $(docker -v)"

빌드 테스트 및 도커 배포
이제 실제 서버에서 도커 빌드 까지 자동화하는 코드를 넣어서 확인해본다.
서버로 프로젝트 코드를 보내야 하기에 Copy files to server 단계를 추가하고 scp-action 을 사용한다.
target 키워드에 코드를 적재할 경로를 작성해주면 된다.
그리고 SSH를 이용해 서버로 접속 단계에서 도커 빌드 스크립트를 적어주면 된다.
# 1. 워크플로우의 이름 (GitHub Actions 탭에 표시됨)
name: NestJS Continuous Integration
# 2. 언제 이 자동화를 실행할지 정의 (트리거)
on:
push:
branches: [ "main" ] # main 브랜치에 코드가 푸시될 때
pull_request:
branches: [ "main" ] # main 브랜치로 합쳐달라는 요청(PR)이 올 때
# paths-ignore: # 특정 파일 수정 시에는 실행하지 않을 경우 명시
# 3. 실행할 작업(Job)들의 모음
# 각 Job은 독립적인 가상 서버 (Runner)에서 실행됨
# 기본적으로 각 Job들은 병렬로 실행됨
# needs: 작업간 순서를 정할 수 있음 ex) 빌드 및 테스트 -> 배포
jobs:
# [1단계] 빌드 및 테스트 단계: 'build-and-test'라는 이름의 작업을 정의
build-and-test:
# 실행 환경: GitHub에서 제공하는 최신 우분투 리눅스 서버 사용. 가장 빠르고 저렴
runs-on: ubuntu-latest
# 작업 내부의 상세 단계(Step)
# 순차적인 명령 집합
steps:
# (1) 가상 서버로 내 레포지토리 코드를 내려받음 (체크아웃)
# uses: 깃헙(actions)에서 만든 액션 레포지토리
# 내 현재 레포지토리 코드를 가상 서버로 옮겨줌
# owner/repo@version 형식
- name: Checkout source code
uses: actions/checkout@v4
# (2) Node.js 환경 설치 (NestJS 실행에 필요)
# uses: 깃헙(actions)에서 만든 액션 레포지토리 setup-node의 v4를 가져다 쓴다는 것.
# 가상서버에 node 환경을 만들어 줌
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.14.0' # 프로젝트에서 사용하는 노드 버전 지정
cache: 'npm' # npm 캐시를 사용하여 다음 실행 시 속도 향상
# (3) 의존성 라이브러리 설치
# 'npm install' 대신 'npm ci'를 권장 (package-lock.json에 명시된 버전 엄격 준수)
- name: Install dependencies
run: npm ci
# (4) 코드 스타일 검사 (Lint)
# 협업 시 일관된 코드 규칙을 지켰는지 확인
#- name: Run Lint
# run: npm run lint
# (5) 테스트 코드 실행 (Unit Test)
# API의 기능들이 정상적으로 작동하는지 확인
#- name: Run Tests
# run: npm run test
# (6) NestJS 빌드 테스트
# TypeScript 코드가 JavaScript로 에러 없이 컴파일되는지 최종 확인
- name: Build project
run: npm run build
# (7) 결과 알림 (성공 시 메시지 출력)
- name: Success Message
if: success()
run: echo "🚀 NestJS CI Pipeline Passed Successfully!"
# [2단계] 배포 (조건부 실행)
# SSH를 이용하여 배포 서버 연결
deploy:
# 조건 1: 앞선 'build-and-test'가 성공해야 함
needs: build-and-test
# 조건 2: '이벤트'가 pull_request가 아니고, 브랜치가 main일 때만 실행
# 즉, PR 단계에서는 deploy를 하지 않음
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# 서버로 파일 전송 (SCP)
- name: Copy files to server
uses: appleboy/scp-action@v0.1.7
with: # Github Secrets에 환경변수 등록
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
password: ${{ secrets.SERVER_PASSWORD }}
# key: ${{ secrets.SERVER_KEY }} # .pem 키파일을 사용할 경우
# port: 22 # 포트 기본값 22
source: "./*" # 로컬의 모든 파일을
target: "/home/user/dev/test-api" # 서버의 이 디렉토리에 옮기겠다.
rm: true # 전송 전 기존 파일 삭제 (선택 사항)
# 서버 접속 후 빌드 (SSH)
- name: Build on Server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
password: ${{ secrets.SERVER_PASSWORD }}
script: |
cd /home/user/dev/test-api # 지정한 디렉토리로 이동
docker build -t test-app .
docker stop test-app || true
docker rm test-app || true
docker run -d --name test-app -p 4445:4445 test-app


아래는 자동 배포 단계가 어떤식으로 진행되는지에 대한 흐름이다.

이로써 깃헙 레포지토리를 사용하고 있다면 깃헙 액션을 이용해 쉽게 자동 배포 파이프라인을 구현할 수 있었다.
수작업을 통한 배포 단계에서 일어날 에러들을 최소화할 수 있고, 배포 단계를 통일시켰기에 배포를 진행하는 책임자끼리 서로 다른 배포 단계를 진행하게 되는 상황도 방지할 수 있을 것이다.
'dev > git' 카테고리의 다른 글
| vscode repository 연결 (0) | 2023.03.26 |
|---|---|
| git clone 후 git hub에 push 가 안됨 (0) | 2023.02.04 |
| 깃 이슈 (0) | 2023.01.13 |
| 깃 다루기.. (0) | 2023.01.13 |