Jenkins 활용해서 Node.js 프로젝트 배포해보기

허창원·2023년 10월 11일
0
post-thumbnail

전체적인 빌드 / 배포 과정

프로젝트 빌드 배포 과정

많은 블로그와 아티클에서 Java를 이용한 기술스택이 많았습니다. 대부분의 과정은 비슷하지만 Java를 이용 시 컴파일 빌드 파일인 jar을 생성해주었습니다.
이번 프로젝트에서는 node.js 기반인 nestJS를 사용했습니다. 전체적인 개발환경부터 빌드/배포 환경의 흐름을 위의 그림으로 정리했습니다. AWS EC2 우분투 내에서 Docker로 Jenkins 이미지 패키지를 다운로드하여 Jenkins를 실행하는 Docker Container를 만들어 사용하였습니다.

Docker 설치 및 설정

## 안전하게 진행하기위해 운영체제에 다운되어 있을 수 있는 파일 삭제하기.
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
## 도커 설치
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

## 설치 확인
sudo docker run hello-world

docker 설치 성공

위와 같이 "Hello from Docker!"가 출력됐다면 성공입니다. Jenkins를 시작하기 전에 AWS EC2 프리티어로 인스턴스를 생성 시 사용할 수 있는 RAM 1GB는 빌드배포 환경에서 문제가 될 확률이 높습니다. 따라서 EC2 메모리 스왑을 통해 디스크 공간을 메모리로 대체하여 여유 메모리를 확보하고 실행해야합니다.

스왑 파일을 사용하여 Amazon EC2 인스턴스에서 스왑 공간으로 사용할 메모리를 할당하는 방법은 무엇입니까?

free -m 으로 확인

설정을 마치고 free -m 명령어를 사용하면 위와 같이 Swap 메모리가 할당된 것을 확인할 수 있습니다. 이어서 터미널에서 root 권한을 도커라는 그룹을 추가하여 부여해 줍니다. 공식문서에 따르면, Docker는 기본적으로 Unix 소켓에 바인딩되어 동작하며, 이 소케의 소유주는 기분적으로 root 사용자입니다. 일반 사용자는 sudo를 사용하여야만 이 소켓에 액세스할 수 있고, Docker 데몬은 항상 root 사용자로 실행됩니다.
sudo를 사용하지 않고도 docker 명령어를 사용하고 싶다면, docker라는 Unix 그룹을 만들어 사용자를 추가할 수 있습니다. Docker 데몬이 시작되면 docker 그룹의 멤버들이 액세스할 수 있는 Unix 소켓이 생성됩니다. 더 자세한 설명은 공식문서를 참고하고 자신의 상황에 맞게 설정해봅시다.

## 도커그룹 생성
sudo groupadd docker
## 사용자를 도커그룹에 추가
sudo usermod -aG docker $USER
## 바로 적용하기
newgrp docker
## sudo 없이 docker 사용해보기
docker run hello-world

Linux post-installation steps for Docker Engine

사용자 추가 설정 전

사용자 추가 설정 후

여기까지 설정을 마친다면, 권한문제가 해결이 됩니다. 하지만 진행 중 "/var/run/docker.sock의 permission denied 발생하는 경우" 아래의 설정을 한번더 해줍니다. 모든 사용자가 파일에 접근할 수 있도록 권한을 부여하는 것이기 때문에 보안상 권장되지는 않습니다.

## 모든사용자에게 권한부여
sudo chmod 666 /var/run/docker.sock
## 도커 로그인하기
docker login

Jenkins 설치 및 설정

도커 설정을 완료했다면, Jenkins를 설치하고 실행할 순서입니다. 이미지 패키지를 사용하여 jenkins를 실행하는 컨테이너를 만들어 jenkins에 접속할 수 있게 합니다. 이 상태로 dockerfile을 사용한 빌드를 진행하면 아래와 같이 docker를 사용할 수 없다라는 오류 메시지와 함께 실패하게 됩니다.

그 이유는 현재 docker를 사용한 별개의 컨테이너 안에서 Jenkins를 실행중이고, 그렇다는 것은 Jenkins에서는 docker를 찾을 수 없게 되는 것입니다. 따라서 Jenkins를 실행중인 컨테이너 내부에 도커 설치가 필요합니다. 이것을 Docker In Docker, DID라고 합니다.

복습하는 차원에서 Jenkins 실행 컨테이너를 만들어 보겠습니다.

docker run \
  --name cicd \
  -p 8080:8080 \
  -e TZ=Asia/Seoul \
  -v /home/jenkins:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  -u root \
  -d \
  --restart unless-stopped \
  jenkins/jenkins:lts

docker run: Docker 컨테이너를 실행하는 명령어
--name (이름 지정): 컨테이너에 이름을 부여하는 옵션으로, 여기서는 "cicd"로 지정되었습니다.
-p 8080:8080: 호스트와 컨테이너 간의 포트 매핑을 설정하는 옵션으로< Jenkins 웹 UI에 접근하기 위한 포트 8080 호스트에, Jenkins 에이전트와 통신하기 위한 포트 8080을 호스트에 매핑합니다.
-e TZ=Asia/Seoul: 환경 변수를 설정하는 옵션으로, 컨테이너 내부의 시간대를 한국 시간대로 설정합니다.
-v /home/jenkins:/var/jenkins_home: 호스트와 컨테이너 간의 볼륨 매핑을 설정하는 옵션으로, Jenkins의 데이터를 저장하는 데 사용되는 디렉토리를 호스트의 /home/jenkins와 컨테이너의 /var/jenkins_home 간에 매핑합니다.
-v /var/run/docker.sock:/var/run/docker.sock: Docker 소켓을 컨테이너 내부로 매핑하는 옵션으로, 컨테이너 내부에서 호스트의 Docker 데몬과 통신할 수 있게 합니다. 
-v /usr/bin/docker:/usr/bin/docker: Docker 실행 파일을 매핑하는 옵션으로, 컨테이너 내부에서 호스트의 Docker 실행 파일을 사용할 수 있게 합니다.
-u root: 컨테이너 내부에서 사용할 사용자를 지정하는 옵션으로, 여기서는 root 사용자로 지정되었습니다.
-d: 컨테이너를 백그라운드 모드로 실행하는 옵션으로, 컨테이너가 백그라운드에서 실행됩니다.
--restart unless-stopped: 컨테이너가 종료될 때 자동으로 다시 시작되도록 설정하는 옵션으로, "unless-stopped"는 명시적으로 중지되지 않은 한 재시작하는 것을 의미합니다. 
jenkins/jenkins:lts: 사용할 Docker 이미지를 지정하는 부분으로, 여기서는 Jenkins의 LTS (Long-Term Support) 버전 이미지를 사용합니다.

docker ps 명령어를 통해 실행 중인 컨테이너를 확인할 수 있습니다. 옵션의 설명을 잘 알아보고 환경에 맞게 설정하면 됩니다.
만약 지정한 포트가 사용중이라는 에러가 발생하면 "sudo lsof -i :8080, sudo kill -9 [PID]"를 작성해봅니다.

## cicd 컨테이너 내부로 진입
docker exec -it cicd bash
## 실제 jenkins 파일이 생성되는 위치
cd /var/jenkins_home/workspace/

여기서 중요한 것은 실제 jenkins를 이용하여 빌드 배포를 진행할 때 jenkins 컨테이너 내부에서 위의 위치에 파일들이 생성됩니다. 이제 컨테이너 내부에서 한번 더 도커를 설치해 줍니다. 방법은 위와 동일합니다.

## 컨테이너 내부로 진입
docker exec -it jenkins bash
## jenkins 컨테이너 안에 docker 설치
apt-get update && \
apt-get -y install apt-transport-https \
  ca-certificates \
  curl \
  gnupg2 \
  zip \
  unzip \
  software-properties-common && \
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey; apt-key add /tmp/dkey && \
add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
$(lsb_release -cs) \
stable" && \
apt-get update && \
apt-get -y install docker-ce

- apt-get update: 패키지 목록을 업데이트합니다.
- apt-get -y install apt-transport-https ca-certificates curl gnupg2 zip unzip software-properties-common: 필요한 패키지를 설치합니다. 이 패키지들은 HTTPS 통신 및 소프트웨어 소스를 추가하는 데 필요합니다.
- curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey: Docker의 GPG 키를 다운로드합니다. 여기서는 호스트의 운영 체제 식별자를 사용하여 해당 GPG 키를 가져옵니다.
- apt-key add /tmp/dkey: 다운로드한 GPG 키를 시스템에 추가합니다.
- add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable": Docker 소프트웨어의 소스를 시스템 소스 목록에 추가합니다. 여기서도 호스트의 운영 체제 정보를 사용하여 Docker 소프트웨어의 저장소를 추가합니다.
- apt-get update: 패키지 목록을 다시 업데이트합니다.
- apt-get -y install docker-ce: Docker Community Edition을 설치합니다. -y 플래그는 설치 도중 나오는 확인 프롬프트에 자동으로 "yes"를 응답하도록 합니다.

docker --version을 통해 잘 설치 되었는지 확인합니다.

Jenkins 컨테이너 안에 도커 설치

본인의 인스턴스 ip주소:8080을 통해 jenkins를 접속 후 새로운 item에서 pipeline을 선택해 줍니다.


GitHub project를 선택하고, 본인의 프로젝트 repository 주소를 설정합니다.


repository와 Jenkins를 깃헙 hook을 통하여 main branch에 소스코드가 업데이트 되면 자동으로 빌드를 실행하도록 해주었습니다.


위와 같이 설정해줍니다. 여기서 Credentials는 자신의 repository가 public이라면 굳이 설정할 필요가 없습니다.


branch가 main으로 되어있기 때문에 /main으로 변경했습니다.

Jenkins Pipeline 구성

모든 설정이 끝났고 이제는 위에서 설정한 것을 실행해줄 스크립트 파일을 작성해줘야합니다. 저는 Dockerfile과 Jenkins를 이용하여 빌드와 배포 구성을 하였습니다. Jenkinsfile은 각각의 단계 설정을 해주고, Dockerfile은 도커를 이용한 빌드를 설정해줍니다. 작성법은 환경에 따라 자유롭게 변경할 수 있습니다.

첫 시도에는 Dockerfile 안에서 RUN npm install 및 build 과정을 시도하였지만, 계속된 타임아웃 오류와 네트워크 오류를 경험하였다. 오랜시간을 들여 파악한 문제가능성은

  • 첫번째로, docker in docker 환경설정에서 네트워크 설정부분이 필요한 구성으로 세팅되어있지 않아서 오류가 발생하는 것
  • 두번째로, 프리티어 성능의 한계로 서버가 죽는 것이었습니다.
    실제로 빌드를 실행하고 htop 명령어를 실행하여 cpu와 memory 자원을 모니터링 해보았는데 cpu가 한계치를 넘어서는 순간이 발생하고 중지되는 것을 확인했습니다. 네트워크 설정 부분은 아직 정확한 구성방법을 찾아내지 못했습니다.

code ECONNRESET

고민의 끝에서 생각한 방법은, Dockerfile내에서 소스코드 빌드과정이 오류가 나니까, 그 부분을 Jenkins 빌드 스테이지로 빼내서 실행 후 만들어진 빌드 파일을 도커라이징하는 방법을 시도하였고 결과적으로 성공했습니다.

pipeline {
    agent {
        docker {
            image 'node:18.16.0'
        }
    }
    
    stages {
        stage('Checkout') {
            steps {
                git branch: 'main',
                    url: 'https://github.com/your project'
            }
            
            post {
                success { 
                    echo 'Successfully Cloned Repository'
                }
                failure {
                    echo 'Fail Cloned Repository'
                }
            }    
        }
        
        stage('Test') { 
            steps {
                echo '테스트 단계와 관련된 몇 가지 단계를 수행합니다.'
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm install'
                sh 'npm run build'
            }
        }
        
        stage('Docker Clear') {
            steps {
                script {
                    echo 'Docker Rm Start, docker 컨테이너가 현재 돌아갈시 실행해야함'
                    def containerId = sh(script: 'docker ps -q -f "name=docker-jenkins-pipeline-test"', returnStdout: true).trim()
                    if (containerId) {
                        sh "docker stop $containerId"
                        sh "docker rm $containerId"
                        sh "docker rmi -f dockerhub-id/docker-jenkins-pipeline-test"
                    } else {
                        echo 'No such container: docker-jenkins-pipeline-test'
                    }
                }
            }

            post {
                success {
                    echo 'Docker Clear Success'
                }
                failure {
                    echo 'Docker Clear Fail'
                }
            }
        }
        
        stage('Dockerizing') {
            steps {
                sh 'echo "Image Build Start"'
                sh 'docker build . -t dockerhub-id/docker-jenkins-pipeline-test'
            }
            
            post {
                success {
                    echo 'Build Docker Image Success'
                }

                failure {
                    echo 'Build Docker Image Fail'
                }
            }
        }
        
        stage('Deploy') {
            steps {
                sh 'docker run --name docker-jenkins-pipeline-test -d -p 8083:5002 dockerhub-id/docker-jenkins-pipeline-test'
            }

            post {
                success {
                    echo 'Deploy success'
                }

                failure {
                    echo 'Deploy failed'
                }
            }
        }
    }
}

업로드중..
스테이지를 확인 할 수 있습니다.

마무리

docker를 사용하여 CI/CD 환경을 수성하면서, 도커의 강점을 체험할 수 있었습니다. 완벽히 해결하지 못한 오류들도 있지만 이번 프로젝트를 진행하면서 개발부터 인프라 운영 과정까지 조금 더 이해의 폭을 넓힐 수 있었습니다.

0개의 댓글