CI/CD 파이프라인 구축

Hyeon·2025년 7월 29일

Nuxt

목록 보기
4/5
post-thumbnail

Nuxt 3 기반의 여수 빅데이터 포털 프로젝트에 대해 CI/CD 환경을 구성했습니다.
CI는 Jenkins, CD는 Docker + Kubernetes를 활용하여 자동화하였으며, 배포 흐름은 아래와 같습니다

개발자가 dev 브랜치에 push → Jenkins가 도커 이미지 빌드 및 푸시 → Kubernetes에 배포

Nuxt 빌드

Nuxt 3에서는 다음과 같이 빌드 및 실행할 수 있습니다.

pnpm build

실행 파일은 .output 디렉토리에 생성되며, 아래 명령어로 실행해볼 수 있습니다.

node .output/server/index.mjs

Docker

Dockerfile은 아래와 같이 multi-stage 방식으로 구성했습니다.

# ================================================
# Base 스테이지: 공통 베이스 이미지 정의
# ================================================
FROM node:22 AS base
# pnpm 설치
RUN npm install -g pnpm

# ================================================
# Deps 스테이지: 전체 의존성 설치
# ================================================
FROM base AS deps
WORKDIR /app
# package.json과 pnpm-lock.yaml 복사 (캐시 활용)
COPY package.json pnpm-lock.yaml ./
# 의존성 설치, 명시된 버전으로만 설치 허용
RUN pnpm install --frozen-lockfile

# ================================================
# Builder 스테이지: 전체 소스코드 복사 후 빌드 수행
# ================================================
FROM base AS builder
WORKDIR /app
# deps 스테이지에서 설치한 모듈 복사 (캐시 활용)
COPY --from=deps /app/node_modules ./node_modules
# 전체 소스 복사
COPY . .
# 빌드 수행 (.output 폴더에 빌드 결과물 생성)
RUN pnpm build

# ================================================
# Runner 스테이지: 빌드 결과물을 이용해 프로덕션 이미지 생성
# ================================================
FROM base AS runner
WORKDIR /app
# 프로덕션 환경 변수 설정
ENV NODE_ENV=production
ENV NUXT_APP_BASE_URL=/management
# builder 스테이지에서 생성된 빌드 결과물 복사
COPY --from=builder /app/.output ./ 
# Nuxt 3 SSR 서버 기본 포트 노출
EXPOSE 3000
# 비루트 사용자로 실행
USER 1001
# Nitro 기반 Nuxt 3 SSR 서버 실행
CMD [ "node", "server/index.mjs" ]

Kubernetes

Nuxt는 SSR 기반이기 때문에 ClusterIP 타입으로 클러스터 내부에서 접근할 수 있도록 구성했습니다.

---
# Management ClusterIP Service (내부 클러스터용)
apiVersion: v1
kind: Service
metadata:
  name: name
  namespace: namespace
spec:
  type: ClusterIP
  selector:
    app: name
  ports:
    - port: 3000
      targetPort: 3000
---
# Management Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: name
  namespace: namespace
spec:
  replicas: 1
  selector:
    matchLabels:
      app: name
  template:
    metadata:
      labels:
        app: name
    spec:
      containers:
        - name: portal
          image: abc.def.ghi/abc/name:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
          securityContext:
            allowPrivilegeEscalation: false
            privileged: false
            readOnlyRootFilesystem: false
            runAsNonRoot: true
          env:
            - name: NUXT_APP_BASE_URL
              value: /management

Jenkins

Jenkins에서는 GitHub에서 dev 브랜치를 기준으로 코드를 받아 빌드 후, 이미지 푸시 및 K8s 배포까지 자동화하였습니다.

import java.time.LocalDate
import java.time.format.DateTimeFormatter

pipeline {
    agent any
    environment {
        WORK_DIR = 'frontend/management'
        KUBE_CONFIG = credentials('KUBECONFIG')
        IMAGE_REPO = 'abc.def.ghi/abc'
        IMAGE_NAME = 'name'
        DEPLOYMENT_NAME = 'name'
        NAMESPACE = 'namespace'
    }
    stages {
        stage('Set Variables') {
            steps {
                script {
                    def today = LocalDate.now().format(DateTimeFormatter.ofPattern('yyyyMMdd'))
                    env.IMAGE_TAG = "${today}-${env.BUILD_NUMBER}"
                }
            }
        }

        stage('Checkout') {
            steps {
                checkout([$class: 'GitSCM',
                    branches: [[name: 'dev']],
                    userRemoteConfigs: [[url: 'https://github.com/abc/def.git', credentialsId: 'github_credential']]
                ])
            }
        }

        stage('Build') {
            steps {
                dir(env.WORK_DIR) {
                    script {
                        withCredentials([usernamePassword(credentialsId: 'harbor_credential', passwordVariable: 'HARBOR_PASSWORD', usernameVariable: 'HARBOR_USERNAME')]) {
                            sh """
                            echo ${HARBOR_PASSWORD} | podman login -u ${HARBOR_USERNAME} --password-stdin ${IMAGE_REPO}
                            podman build --platform=linux/amd64 -t ${IMAGE_REPO}/${IMAGE_NAME}:${IMAGE_TAG} .
                            podman push ${IMAGE_REPO}/${IMAGE_NAME}:${IMAGE_TAG}
                            """
                        }
                    }
                }
            }
        }

        stage('Deploy to Kubernetes') {
            steps {
                dir(env.WORK_DIR) {
                    withCredentials([file(credentialsId: 'KUBECONFIG', variable: 'KUBECONFIG_FILE')]) {
                        sh """
                          export KUBECONFIG=${KUBECONFIG_FILE}
                          sed -i 's|image: ${IMAGE_REPO}/${IMAGE_NAME}:.*|image: ${IMAGE_REPO}/${IMAGE_NAME}:${IMAGE_TAG}|' deployment.yml
                          kubectl apply -f deployment.yml --namespace=${env.NAMESPACE}
                          kubectl rollout status deployment/${env.DEPLOYMENT_NAME} --namespace=${env.NAMESPACE}
                        """
                    }
                }
            }
        }
    }
        
    post {
        success {
            slackSend(
                channel: '#젠킨스',
                color: '#00FF00',
                message: """
                        FE 배포 성공: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}]
                        """
            )
        }
        failure {
            slackSend(
                channel: '#젠킨스',
                color: '#FF0000',
                message: """
                         FE 배포 실패: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}]
                        """
            )
        }
    }
}

0개의 댓글