Nestjs CI CD 프로젝트에서 Docker 최적화 회고

gimseonjin616·2023년 12월 27일
0

들어가며

안녕하세요, Backend 개발자 Kerry 입니다! 저는 이번에 제 Backend 개발자로서의 성장을 위해서 '항해 플러스 3기' 참여하게 됐습니다. 그리고 첫 프로젝트로 NestJS를 기반으로 하는 CI/CD 프로젝트를 진행하게 됐습니다.

이번 프로젝트는 현대 소프트웨어 개발의 핵심 요소인 지속적인 통합과 배포(CI/CD) 전략을 구현하는 데 중점을 두고 있습니다. 이 과정에서 기술적 도전과 학습의 기회가 많았으며, 특히 Docker를 활용한 빌드 및 배포 프로세스의 최적화가 핵심 과제 중 하나였습니다.

이 글에서는 제가 진행한 프로젝트의 아키텍처를 간단히 소개하고, 그 중에서 많은 고민을 한 Dockerfile 최적화 과정을 공유하고자 합니다.

Dockerfile 최적화의 필요성

이번 프로젝트에서 Dockerfile 최적화를 결심한 계기는 초기 NestJS의 Docker 이미지 크기가 예상보다 훨씬 컸기 때문입니다. 기본 프로젝트 설정 후 Docker로 빌드했을 때, 이미지 크기가 무려 750MB에 달했습니다. 이는 심지어 MySQL의 최신 이미지(618MB)보다도 더 큰 용량이었습니다.

초기 nestjs docker imageMysql image
750mb618mb

이러한 비교를 통해 Docker 이미지의 크기 최적화가 필요함을 실감했습니다. 여러 최적화 방법을 적용한 결과, 최종적으로 이미지 크기를 274.6MB까지 줄일 수 있었습니다. 이 글을 통해 그 과정과 학습 포인트를 공유하며, Dockerfile 최적화가 프로젝트 효율성에 어떤 영향을 미쳤는지 설명하고자 합니다.

프로젝트 아키텍처

Step 1. GitHub Repository에서의 Pull Request와 Merge

프로젝트는 GitHub 저장소를 중심으로 운영됩니다. 개발 과정에서는 다음과 같은 CI/CD 전략을 채택했습니다:

개발 브랜치 (dev branch): 여기서는 배포 전 기능을 테스트합니다. 개발 환경에 코드를 배포하여 기능성을 검증합니다.
QA 브랜치 (release-*): 이 브랜치는 특정 버전의 기능을 배포하기 전 QA(품질 보증) 단계를 위해 사용됩니다. 릴리스 환경에 배포하여 최종 점검을 진행합니다.
메인 브랜치 (main branch): 사용자에게 제공되는 현재 코드들이 이 브랜치에 있으며, 프로덕션 환경에 배포됩니다.

Step 2. GitHub Actions을 이용한 CI/CD Trigger

프로젝트의 각 단계는 GitHub Actions을 활용하여 자동화됩니다. 이는 코드 변경사항을 감지하고, 필요한 빌드 및 배포 절차를 자동으로 실행합니다. 이 절차는 다음 단계들을 포함합니다:

Build & Test 단계: 소스 코드를 빌드하고 테스트를 실행합니다.
Docker Build 단계: Docker 이미지를 생성합니다. 이때 각 브랜치에 맞게 version을 명시합니다.
Docker Release 단계: 생성된 이미지를 AWS ECR에 등록합니다.
Deploy 단계: AWS ECR에서 이미지를 가져와 AWS ECS & Fargate를 활용하여 배포합니다.

Step 3: Docker Release 단계

이 단계에서는 AWS ECR에 Docker 이미지를 등록합니다. 이는 후속 배포 프로세스의 기반이 됩니다.

Step 4: Deploy 단계

마지막 단계에서는 AWS ECR에 저장된 Docker 이미지를 사용하여 AWS ECS & Fargate를 통해 애플리케이션을 배포합니다. 이는 효율적이고 안정적인 배포 프로세스를 보장합니다.


최적화된 Dockerfile

아래는 프로젝트에 최종으로 선택된 최적화된 Dockerfile의 구조입니다.

Dockerfile

# Build Stage
FROM node:18-alpine as builder

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

# Production Stage
FROM node:18-alpine

WORKDIR /usr/src/app

COPY --from=builder /usr/src/app/package*.json ./

RUN npm install --production

COPY --from=builder /usr/src/app/dist ./dist

ENTRYPOINT ["npm", "run", "start:prod"]

.dockerignore

node_modules/
dist/

.git
.gitignore
.dockerignore
Dockerfile

Docker 최적화 과정

이번 프로젝트에서 Docker 파일을 최적화하기 위해서 선택한 방법은 크게 'docker ignore 추가'와 'Docker 환경 분리' 두 가지입니다.

Step 1: .dockerignore 추가

이 단계에서는 .dockerignore 파일을 사용하여 Docker 이미지 빌드 과정에서 불필요한 파일과 디렉토리를 제외했습니다. 이 파일은 Docker 컨텍스트에 포함되지 않아야 할 파일들을 지정합니다. 예를 들어, 로컬 개발 환경에서만 필요한 파일, 중간 빌드 파일, 로그 파일 등을 제외하여 빌드 프로세스의 효율성을 향상시키고 최종 이미지의 크기를 줄일 수 있습니다.

.dockerignore

node_modules/
dist/

.git
.gitignore
.dockerignore
Dockerfile

Step 2: Docker 환경 분리

빌드하는 과정에서 Docker 환경을 분리하도록 했습니다. 멀티-스테이지 빌드를 사용하여 빌드 단계와 프로덕션 단계를 분리함으로써, 최종 이미지에는 필요한 파일과 설정만 포함되도록 했습니다. 이렇게 하면 빌드 단계에 필요한 종속성과 도구는 최종 이미지에 포함되지 않아 이미지 크기가 크게 감소합니다.

Before

FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

ENTRYPOINT npm run start:prod 

After

# Build Stage
FROM node:18-alpine as builder

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

RUN npm run build

# Production Stage
FROM node:18-alpine

WORKDIR /usr/src/app

COPY --from=builder /usr/src/app/package*.json ./

RUN npm install --production

COPY --from=builder /usr/src/app/dist ./dist

ENTRYPOINT ["npm", "run", "start:prod"]

Docker 빌드 성능 비교 결과

이제 위에 채택한 두 가지 전략이 얼마나 효과적인지 테스트 해보려고 합니다. 테스트 방식은 Docker 빌드 환경에 따른 성능 비교를 해보려고 합니다. 성능 측정 방식은 docker system prune -a 명령어를 활용해서 Docker 캐시를 지운 상태에서 각 환경을 5회씩 빌드하여 얻은 결과입니다. 측정은 평균 build 시간과 이미지 크기를 선택했습니다.

표: Docker 빌드 테스트 결과

설정평균 빌드 시간 (초)이미지 크기 (MB)
Without .dockerignore48.80749.5
With .dockerignore35.10463.5
With .dockerignore & Env Separation43.36274.6

그래프: 빌드 시간 비교

그래프: 이미지 크기 비교

결과

빌드 시간 분석

우선 .dockerignore를 추가함으로써, 평균 빌드 시간은 48.80초에서 35.10초로 대폭 감소했습니다. 이는 불필요한 파일과 디렉토리가 Docker 빌드 컨텍스트에서 제외되어 빌드 과정이 간소화되었음을 나타냅니다. 그 후 환경 분리를 추가한 경우에는 빌드 시간은 약간 증가했지만 이는 분리된 환경 만큼 추가 빌드 단계를 거치는 데 필요한 시간 때문입니다. 그리고 아래에서 최종 이미지 크기 감소라는 중요한 이점을 감안하면 충분히 감수할만한 범위입니다.

이미지 크기 분석

최적화 과정에서 가장 눈에 띄는 결과는 이미지 크기의 감소였습니다. .dockerignore를 사용한 경우 이미지 크기가 749.5MB에서 463.5MB로 감소했으며, 환경 분리를 추가하면서 274.6MB로 더욱 줄었습니다. 이는 불필요한 파일을 제거하고, 빌드와 프로덕션 환경을 분리하여 필요한 파일만 포함시키는 전략의 효과를 명확하게 보여줍니다.

회고: Docker 최적화를 마치며

이번 프로젝트에서 Dockerfile의 최적화 과정을 경험하며 정말 많은 것을 얻었습니다. 단순히 기술적인 목표를 넘어서, 이 과정은 성능 최적화에 대한 제 열정을 되살리고 새로운 감각을 일깨워주는 기회였습니다.

기술적 성장

현대 웹 애플리케이션 개발에서 Docker의 역할은 매우 중요합니다. 이번 최적화 과정을 통해 Docker의 기본 개념, 이미지 및 컨테이너 관리, 그리고 Docker의 내부 작동 원리에 대한 이해를 얻었습니다. 이러한 지식은 앞으로의 프로젝트에도 큰 자산이 될 것입니다.

성능에 대한 새로운 인식

과거 빠듯한 일정으로 인해 성능 최적화보다 기능 개발에 치중했던 경험을 되돌아보게 됐습니다. 이번 프로젝트는 성능 최적화에 대한 중요성을 다시금 일깨워주었습니다. 최적화 과정을 통해 성능 향상에 대해 깊게 생각해보게 됐고, 그 과정에서 큰 즐거움을 느꼈습니다.

개인적인 소감

이 글을 작성하며 제 경험을 되돌아보는 것은 단순히 기술적인 회고를 넘어, 개발자로서의 성장과 열정을 재확인하는 시간이었습니다. 이 글이 독자 여러분께도 프로젝트의 성공을 위한 최적화의 중요성과 그 과정에서 얻는 만족감을 전달하길 바랍니다.

Kerry의 개발 이야기는 여기서 끝나지 않습니다! 다음 프로젝트에서 더욱 흥미로운 도전과 성장을 기대하며, 여러분도 즐거운 개발 여정을 이어가시기를 바랍니다!

profile
to be data engineer

0개의 댓글

관련 채용 정보