이번 게시글에서는 Docker를 이용한 Jenkins 설치 및 CI/CD 구축과정을 기술할 것이다. 구축방법에 대해서는 다른 좋은 게시글이 많으니, 해당 게시글에서는 오류해결 방법과 코드에 관련된 내용에 대해서 중점적으로 다룰 예정이다.
EC2와 AWS 프리티어는 1GB 메모리를 제공해, Jenkins 운영 중 메모리 부족으로 멈출 수 있다. 이를 막으려면 스왑 메모리 설정이 필요하다.
// swap 파일 만들기
$ sudo mkdir /var/spool/swap
$ sudo touch /var/spool/swap/swapfile
$ sudo dd if=/dev/zero of=/var/spool/swap/swapfile count=2048000 bs=1024
// swap 파일 설정
$ sudo chmod 600 /var/spool/swap/swapfile
$ sudo mkswap /var/spool/swap/swapfile
$ sudo swapon /var/spool/swap/swapfile
// swap 파일 등록
$ sudo vim /etc/fstab
// 파일 끝에 다음 입력 후 저장
/var/spool/swap/swapfile none swap defaults 0 0
// 메모리 상태 확인
$ free -h
해당 과정을 진행하여 스왑 메모리 설정을 수행한다.
Docker 없이 Jenkins를 우분투에 설치하면 여러 환경 설정을 해야 하지만, Docker 사용하면 간단한 명령어로 설치 가능하다.
우선 아래 링크를 참고해서 EC2서버에 Docker를 설치해 준다.
그리고 다음 명령어로 Jenkins 실행한다.
docker run -d -p 8080:8080 --name jenkins jenkins/jenkins:jdk17
하지만 해당 명령어를 이용해서 Jenkins를 실행할 경우, Jar 파일을 이용한 배포는 가능하지만 jenkins 실행하는 서버에 docker가 없어서 docker를 이용한 배포를 구현할 시에는 여러 오류가 발생할 것이다. 해당 오류들의 해결 방법을 아래에서 자세히 다루겠다.
이 오류는 Jenkins 컨테이너 안에서 Docker 명령을 못 찾을 때 나타난다. Docker 컨테이너가 기본적으로 Docker 환경을 포함하지 않아서 생기는 일이다. 해결책은 호스트의 Docker 소켓을 Jenkins 컨테이너와 공유해서 컨테이너 안에서도 호스트의 Docker를 쓸 수 있게 하는 것이다.
docker run -d -it -v /var/run/docker.sock:/var/run/docker.sock -p 8080:8080 --name jenkins jenkins/jenkins:jdk17
이 명령어는 Docker 컨테이너를 실행하면서 호스트의 Docker 소켓(/var/run/docker.sock)을 컨테이너 내부에 마운트한다. 이렇게 하면 컨테이너 내부에서 실행되는 Jenkins 프로세스가 호스트 시스템의 Docker 데몬을 직접 호출할 수 있게 된다.
docker cp /usr/bin/docker jenkins:/usr/bin/docker
이 추가 명령어는 호스트 시스템의 Docker 실행 파일을 Jenkins 컨테이너 내부로 복사한다. 이는 컨테이너 내부에서 Docker 클라이언트를 사용할 수 있도록 하기 위함이다.
이 오류는 Jenkins 컨테이너가 호스트 시스템의 Docker 데몬에 접근할 때 권한 문제로 발생한다. 기본적으로, /var/run/docker.sock 소켓은 root 사용자와 Docker 그룹에 속한 사용자만 접근할 수 있다. Jenkins 컨테이너 내부에서 실행되는 프로세스는 일반적으로 jenkins 사용자 권한으로 실행되므로, 이 사용자가 Docker 소켓에 접근하려 할 때 권한 거부 오류가 발생한다.
sudo docker exec -itu0 jenkins bash
chown jenkins:jenkins /var/run/docker.sock
이 명령어는 Jenkins 컨테이너 내부에서 root 권한으로 bash 세션을 시작한 다음, Docker 소켓의 소유권을 jenkins 사용자로 변경하여, 해당 사용자가 Docker 소켓에 접근할 수 있도록 한다. 이렇게 하면 Jenkins 내부에서 Docker 명령어를 실행할 때 권한 문제가 해결된다.
Jenkins에서 CI/CD 프로젝트를 구축하기 위해선 아이템(Item)을 생성해야 한다. 하나의 Jenkins 서버에서 여러 개의 아이템을 만들 수 있으며, 각 아이템은 개발자가 설정에 따라 다르게 동작한다. Jenkins에서 아이템을 만드는 주요 방법에는 FreeStyle과 Pipeline이 있다. 나는 Pipeline이 방식을 이용하여 아이템을 생성하였다. 아래는 내가 작성한 파이프 라인 코드이다.
pipeline {
agent any
environment {
IMAGE_NAME = "jeyongsong/automaticstore"
CONTAINER_NAME = "automaticstore"
SSH_CONNECTION = "ubuntu@ip-172-31-38-96"
SSH_CONNECTION_CREDENTIAL = "aws_key"
SSH_HOST = "ip-172-31-38-96"
}
stages {
stage("Setup") {
steps {
script {
BRANCH = "main"
EXECUTE_PROFILE = "dev"
currentTime = new Date().format('yyyyMMddHHmmss', TimeZone.getTimeZone('Asia/Seoul'))
TAR_FILE = "automaticstore_${currentTime}.tar"
}
}
}
stage('Git Clone') {
steps {
git branch: BRANCH, url: 'https://github.com/joon6093/O2O_Automatic_Store_System_Demo.git'
withCredentials([GitUsernamePassword(credentialsId: 'submodule_security_token', gitToolName: 'Default')]) {
sh 'git submodule update --init --recursive'
}
}
}
stage('Prepare SSH') {
steps {
sshagent(credentials: [SSH_CONNECTION_CREDENTIAL]) {
sh "ssh-keyscan -H ${SSH_HOST} >> ~/.ssh/known_hosts"
}
}
}
stage('Docker Image Build') {
steps {
sh "docker build -t ${IMAGE_NAME} ."
sh "docker save ${IMAGE_NAME} > ${TAR_FILE}"
}
}
stage('Transfer Image to WAS Server') {
steps {
sshagent(credentials: [SSH_CONNECTION_CREDENTIAL]) {
sh "scp ${TAR_FILE} ${SSH_CONNECTION}:~/"
}
}
}
stage('Deploy on WAS Server') {
steps {
sshagent(credentials: [SSH_CONNECTION_CREDENTIAL]) {
// Stop and remove the current container
sh "ssh ${SSH_CONNECTION} 'docker stop ${CONTAINER_NAME} || true && docker rm ${CONTAINER_NAME} || true'"
// Remove the old image
sh "ssh ${SSH_CONNECTION} 'docker rmi ${IMAGE_NAME} || true'"
// Load the new image
sh "ssh ${SSH_CONNECTION} 'docker load -i ~/${TAR_FILE}'"
// Run the new container
sh "ssh ${SSH_CONNECTION} 'docker run -d -p 8080:8080 -m 600m --cpus=0.7 -e PROFILE=${EXECUTE_PROFILE} --name ${CONTAINER_NAME} ${IMAGE_NAME}'"
}
}
}
}
}
해당 내용은 아래에서 자세히 설명하겠다. 코드가 너무 길어진 관계로 코드를 이용해서 설명하지는 않겠다.
environment 블록에서는 빌드 및 배포에 필요한 환경 변수를 설정한다.
초기 설정 단계에서 Git 브랜치, 실행 환경 프로필, 현재 시간을 기반으로 한 tar 파일 이름을 설정한다.
소스 코드를 GitHub에서 클론한다. 특정 브랜치를 지정하고, 필요하면 서브모듈을 초기화 및 업데이트한다.
원격 서버에 안전하게 접속하기 위해 SSH 키를 known_hosts 파일에 추가한다.
Dockerfile을 사용해 Docker 이미지를 빌드하고, 이 이미지를 tar 파일로 저장한다.
빌드한 Docker 이미지 tar 파일을 SSH를 통해 웹 애플리케이션 서버로 전송한다.
원격 서버에서 Docker 컨테이너를 관리하는 단계다.
파이프라인이 잘 실행된 모습이다.
Jenkins 서버가 WAS 서버에 접속하여 기존에 실행 중이던 컨테이너를 정지하고 제거하는 모습이다.
Jenkins 서버가 WAS 서버에 접속하여 이전 이미지를 제거하는 모습이다.
Jenkins 서버가 WAS 서버에 접속하여 새 이미지를 로드하는 모습이다.
Jenkins 서버가 WAS 서버에 접속하여 새 이미지를 기반으로 새 컨테이너를 실행하는 모습이다.
WAS서버에서 Jenkins 서버에서 보내준 .tar 파일을 잘 전송받은 모습이다. 또한 새 컨테이너를 잘 실행하는 모습이다.
WAS 서버의 인바운드 규칙을 편집하여 SSH port를 자기자신의 IP와 Jenkins 서버의 Private Ip로 설정하여 더욱 더 안전하게 설정하였다.
Jenkins 서버에도 https를 적용하였다.
AWS의 Route 53 서비스의 호스팅 영역 설정을 통해서 도메인 주소는 WAS서버로 이용하고 Jenkins.도메인주소는 Jenkins 서버로 이용하도록 하였다. 해당 기능을 이용해서 Jenkins 서버를 쉽게 접속할 수 있도록 하였다.
또한 Jenkins 로드밸런서의 인바운드 규칙을 편집하여 HTTPS의 접속 IP를 내 PC로만 설정하여 자신만 접속할 수 있도록 하였다.
Jenkins.도메인주소를 이용하여 Jenkins의 설정을 할 수 있는 모습이다.
Git hub web hook을 이용해서 Git hub 저장소의 push 이벤트가 발생하였을 경우, 자동으로 Jenkins의 아이템이 실행하도록 할 수 있다.
Github web hook 설정 방법은 다른 게시글에서 자세히 다루고 있다. 여기서는 간단히 해당 게시글을 추천한다.
Github web hook의 ip주소는 아래 링크에서 알 수 있다.
Git hub web hook 설정을 마친 모습이다.
push 이벤트가 발생할 시 자동으로 Jenkins CICD 아이템이 실행 되는 모습이다.
Docker를 활용하여 Jenkins 설치 및 CI/CD 구축을 성공적으로 마쳤다. CI 도구로는 GitHub Action을, CD 도구로는 Jenkins를 사용하기로 결정했다. GitHub Action을 선택한 이유는 코드 변경이 발생했을 때 문제를 신속하게 발견할 수 있기 때문이다.
CD 도구로 Jenkins를 선택한 가장 큰 이유는 보안 때문이다. GitHub Action으로 CD를 구성할 경우, 웹 애플리케이션 서버(WAS)의 SSH 접속 IP를 공개해야 하는 문제가 있다. GitHub Action의 IP는 지속적으로 변하는 반면, Jenkins를 사용하면 WAS 서버의 SSH 접속 IP를 제한하여 시스템을 더 안전하게 운영할 수 있다.
Docker Hub를 이용한 배포 방식도 고려했으나, 이 방법은 프라이빗 저장소 사용 시 발생하는 추가 비용 때문에 최종적으로 선택하지 않았다. AWS의 AWS CodeDeploy 역시 비용 문제로 고려 대상에서 제외되었다. 반면, Jenkins는 AWS 프리티어 계정을 사용함으로써 추가 비용 없이 운영할 수 있다.
또한 Jenkins는 앞에 비교했던 방식보다 직관적인 인터페이스를 통해 CD 작업을 쉽게 진행할 수 있다는 장점이 있다.
결국, GitHub Action과 Jenkins의 조합은 현재 나의 개발 환경에서 비용 효율적이며 보안 측면에서도 우수한 CI/CD 파이프라인을 구축할 수 있는 최적의 솔루션으로 판단된다.