사내 프로젝트의 프론트엔드 개발과 함께 배포 관리를 맡게 되었습니다. 시스템 특성상 두 곳의 폐쇄망을 사용하는 특수한 상황이었기에 효율적인 CI/CD 구축이 필요했습니다.
사내망 코드저장소(Gitlab)
저희 팀의 코드 저장소는 사내망에서 운용되고 있습니다.
배포용 NCP Private Cloud 환경
프로젝트 조건 중 NCP Private Cloud 환경에 VPN을 통해 접근하여 K8S 클러스터에 서비스를 등록해야 합니다.
위와 같은 환경에서 CI/CD 파이프라인을 구축하면서 공부한 내용을 정리하였습니다.
전체적인 플로우는 CI와 CD가 분리되어 있습니다.
FROM node:18 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build:prod
FROM nginx:stable-alpine
COPY --from=build /app/dist/apps/inspection /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
위의 Dockerfile은 두 단계로 구성됩니다.
Node.js 이미지를 사용하여 애플리케이션을 빌드합니다. 소스 코드와 package.json 파일을 복사한 후, 의존성을 설치하고 npm run build:prod 명령어를 실행하여 프로덕션용 빌드를 생성합니다.
Nginx 이미지를 사용하여 빌드된 애플리케이션을 서비스합니다. 빌드 단계에서 생성된 파일들을 Nginx의 웹 루트 디렉터리인 /usr/share/nginx/html
로 복사하고, 기본 Nginx 설정 파일을 제거한 후, 사용자 정의 Nginx 설정 파일을 복사합니다. 마지막으로, Nginx 서버를 실행합니다.
server {
listen 80;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /inspection/ {
proxy_pass http://localhost:80/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
위의 Nginx 설정 파일은 HTTP 요청을 처리하는 방법을 정의합니다.
기본 서버 블록에서는 포트 80을 리슨하고, 로그 파일의 경로를 설정합니다.
루트 경로(/
)의 경우, 요청된 파일이 존재하지 않으면 index.html
파일을 반환합니다. SPA(Single Page Application)의 라우팅 문제를 해결하기 위한 설정입니다.
/inspection/
경로로 들어오는 요청은 프록시 패스로 로컬호스트의 포트 80으로 전달됩니다. 이때 몇 가지 헤더 정보를 함께 전달하여 클라이언트의 정보를 유지합니다.
위 설정을 통해 Docker 컨테이너에서 Nginx를 사용하여 Node.js로 빌드한 애플리케이션을 서비스할 수 있습니다.
GitLab CI는 GitLab의 지속적 통합(CI) 도구로, 소프트웨어의 자동 빌드, 테스트, 배포를 지원합니다. .gitlab-ci.yml
파일을 통해 파이프라인을 정의하며, 각 Job은 특정 단계(Stage)에 속합니다.
.gitlab-ci.yml
파이프라인 (Job 묶음) 스크립트를 작성하여 GitLab CI를 통해 정적/유닛/통합/빌드 테스트를 수행합니다. 테스트를 통과하면 이미지 빌드 후 레지스트리에 저장합니다.
stages:
- test
- build
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
merge_test:
stage: test
image: node:18.17.1
only:
- merge_requests
script:
- npm install
- npm run static-test # 린트 정적 테스트
- npm run test # 유닛/통합테스트
- npm run build:prod # 빌드 테스트
tags:
- my-tag
docker_build_and_save:
stage: build
image: docker:latest
services:
- docker:dind
only:
- merge_requests
script:
- docker build -t inspection:latest .
- docker save -o inspection_latest.tar inspection:latest
# ...
tags:
- my-tag
after_script:
- echo "End CI"
이 설정으로 GitLab CI는 코드 변경 시 자동으로 테스트와 빌드를 수행하고, Docker 이미지를 생성해 레지스트리에 저장합니다.
배포 패키지는 Gitlab-CI에서 빌드한 이미지, 선언적 YAML 파일, 그리고 NCP에서 배포를 실행할 스크립트를 묶어서 구성한 것 입니다.
아래의 쉘 스크립트를 사용하여 버저닝된 배포패키지 생성과 scp를 사용해 NCP Cloud로 전송합니다.
scp?
ssh 원격 접속 프로토콜을 기반으로 한 "SecureCopy"의 약자로 원격지에 있는 파일과 디렉터리를 보내거나 가져올 때 사용하는 파일 전송 프로토콜입니다.
if [ -z "$1" ]; then
echo "Usage: $0 <DOCKER_TAG>"
exit 1
fi
DOCKER_TAG=$1
# 1. 루트에 deployment 디렉토리 생성
mkdir -p ./deployment
# 2. deployment.yaml 파일 생성
cat <<EOF > ./deployment/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: ...
name: ...
labels:
app : ...
spec:
replicas: ...
selector:
matchLabels:
app: ...
strategy:
type: RollingUpdate
template:
metadata:
labels:
app: ... # 레이블과 선택자에서 사용된 서비스 이름과 일치해야 합니다.
spec:
containers:
- env:
...
image: {registry}/inspection:$DOCKER_TAG # 이미지 위치와 버전이 일치해야 합니다.
name: ... # 서비스 이름에 맞게 사용자 정의 가능합니다.
ports:
- containerPort: 80
volumeMounts:
- mountPath: ...
name: ...
volumes:
- name: ...
persistentVolumeClaim:
claimName: ...
restartPolicy: Always
imagePullSecrets:
- name: regcred # 이미지 풀 시크릿의 이름과 일치해야 합니다.
EOF
# 3. script.sh 파일 생성
cat <<EOF > ./deployment/script.sh
sudo docker load -i inspection_latest.tar
sudo docker tag inspection:latest {registry}/inspection:$DOCKER_TAG
sudo docker push {registry}/inspection:$DOCKER_TAG
kubectl apply -f deployment.yaml
EOF
chmod +x ./deployment/script.sh
# 4. 깃랩 서버에서 최신 이미지 파일 가져오기
scp {깃랩서버_ID}@{깃랩서버_IP}:{깃랩서버_IP}:{파일경로} ./deployment
# 5. 패키징된 배포 파일 NCP 서버로 업로드
scp -r deployment/* {NCP서버_ID}@{NCP서버_IP}:{타겟경로}
echo "🚀 검수앱 빌드 패키지: $DOCKER_TAG NCP 업로드 성공"
echo "What's Next ? NCP에서 업로드된 스크립트를 실행하세요. (README 참고)"
위 스크립트를 npm 스크립트에 등록한 후, 도커태그 인자와 함께 실행하면, 아래와 같은 버저닝 된 배포패키지가 NCP Private Cloud에 전송됩니다.
# 배포패키지 구성
.
├── deployment.yaml
├── inspection_latest.tar
└── script.sh
NCP 환경의 K8S 클러스터에서는 프로젝트를 구성하는 여러 앱을 Ingress를 통해 각각의 서비스로 로드밸런싱하고 있습니다.
NCP에 ssh로 접속 후, sh script.sh
명령어를 통해 배포패키지 중 배포 스크립트가 실행됩니다. 배포스크립트는 deployment.yaml를 apply하여 기존 인프라에 업데이트 된 컨테이너를 올리면서 배포가 완료됩니다.
Kubernetes는 컨테이너화된 애플리케이션의 배포, 확장 및 관리를 자동화하기 위한 오픈 소스 시스템입니다.
Kubernetes는 '바람직한 상태'를 유지해주는 것이 기본적인 아이디어입니다.
바람직한 상태란 사용자가 바라는 환경을 의미하며, 구체적으로는 "컨테이너는 OO개, 볼륨은 XX개로 구성하라" 등의 명세(Spec)를 YAML 파일로 정의하고, 자동으로 유지합니다.
즉 선언적 배포 방식으로, 사용자는 원하는 상태를 정의하고, Kubernetes가 유지하도록 합니다.
Ingress
Ingress는 클러스터 외부에서 내부 서비스로 HTTP 및 HTTPS 경로를 라우팅하는 데 사용되는 Kubernetes 리소스입니다. Ingress는 단일 IP 주소로 여러 서비스에 대한 경로 기반 접근을 제공하며, SSL 종료 및 가상 호스트 기반 라우팅과 같은 고급 기능을 지원합니다.
- Ingress의 로드밸런싱
: 기존에 알고 있던 로드밸런싱은 하나의 요청을 여러 서버에 분산하는 것이었습니다. 예를 들어, www.myblog.com에 들어오는 요청을 서버 A, 서버 B로 나누어 주는 것입니다. 이는 OSI 7계층 중 전송 계층에서 이루어지는 로드밸런싱(L4)입니다.
Ingress에서 이루어지는 L7 기반 로드밸런싱은 응용 계층에서 www.myblog.com으로 접근할 때 /post
와 /search
요청을 각각 담당 서버들로 분산하는 개념입니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
...
- host: something.net
http:
paths:
- backend:
service:
name: serviceName
port:
number: 3001
path: /inspection
pathType: Prefix
...
Service
Service는 Kubernetes 클러스터 내에서 실행 중인 파드(Pod) 집합에 대한 네트워크 접근 방식을 정의합니다. Service는 클러스터 내부 또는 외부에서 파드로의 안정적인 접근을 가능하게 합니다. 서비스에는 여러 종류가 있으며, 가장 일반적인 유형은 ClusterIP, NodePort, LoadBalancer입니다.
# service.yaml
apiVersion: v1
kind: Service
metadata:
namespace: ...
name: ...
spec:
type: NodePort
ports:
- protocol: TCP
port: 3001
targetPort: 80
selector:
app: ...
Deployment
Deployment는 파드와 레플리카셋(ReplicaSet)을 관리하는 Kubernetes 리소스입니다. Deployment는 선언적 방식으로 파드의 배포와 스케일링을 자동화하며, 롤링 업데이트 및 롤백 기능을 제공합니다. 이를 통해 새로운 애플리케이션 버전을 중단 없이 배포할 수 있습니다.
Pod
Pod는 Kubernetes에서 배포할 수 있는 가장 작은 단위로, 하나 이상의 컨테이너를 포함합니다. 파드는 동일한 네트워크 네임스페이스를 공유하고, 동일한 볼륨에 접근할 수 있으며, 일반적으로 단일 애플리케이션 인스턴스를 실행합니다. 파드는 단명(short-lived) 특성이 있으며, 필요에 따라 생성되고 삭제됩니다.
처음 배포를 수동으로 배포를 진행할 때, 배포 가이드를 작성하였지만 한 페이지로도 부족했습니다. 그러나 프로세스 설계, 스크립트 작성 및 CI/CD 파이프라인을 구축한 후 2줄의 명령어로 배포가 끝났습니다. 이를 통해 서비스의 안정성과 배포의 효율성, 생산성을 향상할 수 있었습니다.
CI/CD 구축을 리서치하는 동안, 한 블로그에서 "CI/CD에 정답은 없다"는 말을 보았습니다. 이는 각 시스템마다 특성이나 배포 성향이 다르기 때문입니다. 최고의 방법은 아니어도 해당 프로젝트에서 최선의 방법을 찾기 위해 노력했습니다. 팀원분들에게 배포하기 훨씬 편해졌다는 피드백을 듣고 보람찼습니다.
앞으로도 지속적으로 개선하고 발전시켜 더 나은 개발 환경을 만들어 나가고자 합니다.
안녕하세요, 네이버 클라우드 플랫폼입니다.
네이버클라우드의 기술 콘텐츠 리워드 프로그램 ‘이달의 Nclouder(7월)’ 도전자로 초대합니다 🙂
네이버 클라우드 플랫폼 서비스와 관련된 모든 주제로 8/2(금) 23시까지 신청 가능합니다. (*7월 작성 콘텐츠 한정 신청 가능)
Ncloud 크레딧을 포함한 다양한 리워드가 준비되어 있으니 많은 관심 부탁드립니다!
자세한 내용은 아래 링크에서 확인부탁드립니다.
https://blog.naver.com/n_cloudplatform/223508217521
신청 링크
https://navercloud.typeform.com/to/lF8NUaCF