새로운 프로젝트를 진행하며 서버 배포 자동화를 위해 Jenkins를 도입하기로 결정했습니다. 구글링하며 시도하면 간단할 줄 알았지만, 정말 고생을 많이 했습니다 ㅠㅠ,, 몇 날 며칠을 밤을 새우며 결국 완성해냈고, 그 과정을 공유하고자 합니다! 다른 분들은 부디 편히 완성하시길 바라며, 들어가보겠습니다 :)
이전 다수의 프로젝트 서버 배포는 로컬에서 개발한 springboot 프로젝트를 수동으로 빌드하는 방식을 사용했었습니다. 따라서, 프로젝트에서 수정사항이 생길 때마다 매번 재빌드해 배포해야하는 번거로움이 있었습니다. 이는 정말 불편하게 느껴집니다. 따라서, 배포 자동화에 관심이 생겨 공부를 진행하다 CI/CD 를 접하게 되었고, 이번 프로젝트에 적용하기로 결정했습니다. 사실 한 번 써보고 싶었던게 제일 클지도...
저희 프로젝트의 서버 아키텍처는 다음과 같이 배포 자동화 시스템을 구축했습니다.
Jenkins와 Docker 를 이용해 배포가 자동화되는 과정을 간단히 살펴보자면, 다음과 같습니다.
우선, 개발을 진행할 Springboot 프로젝트를 생성해 Dockerfile
을 생성합니다. 이를 기반으로 이미지 빌드를 하고, 실행하게 됩니다.
파일 이름은 반드시 Dockerfile
이어야 합니다. DockerFile, dockerfile 등 다른 이름이면 인식이 안됩니다.
Dockerfile
의 내용은 다음과 같이 작성합니다.
FROM openjdk:11
LABEL authors="USER_NAME"
ARG JAR_FILE=build/libs/jenkins-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} docker-springboot.jar
ENTRYPOINT ["java", "-jar", "/docker-springboot.jar", ">", "app.log"]
Dockerfile 내용에 대해서는 여기서 자세히 다루지 않겠습니다. 자세히 알고 싶다면 아래 블로그를 참고하면 좋을 것 같습니다:)
도커파일 작성법
하나 주의할 점은 ARG
의 jar 파일에서 jenkins-0.0.1-SNAPSHOT의 jenkins 부분에 본인의 프로젝트 이름을 넣어주시면 됩니다.
저는 GCP 환경에서 배포를 진행하기 때문에 GCP를 기준으로 설명하겠습니다. 우선 Jenkins 서버를 위한 인스턴스를 하나 생성하고 Docker를 통해 Jenkins를 빌드하겠습니다.
GCP → Compute Engine → VM 인스턴스로 들어가 상단의 인스턴스 만들기
로 Jenkins를 위한 인스턴스를 생성합시다. 저는 부팅 디스크의 운영체제를 Ubuntu로 선택했습니다.
또한, 방화벽 탭에서 HTTP, HTTPS 트래픽을 모두 허용시켜 줍니다.
생성하고, 접속해봅시다!
Jenkins를 Docker를 통해 실행하기 위해, 우선 Docker를 설치해줍시다.
저는 Ubuntu 환경에서 진행했기 때문에 아래와 같이 설치하겠습니다.
다른 운영체제를 사용 중이라면, 아래 docker docs에서 자신에게 맞는 운영체제를 선택해 따라 설치해주면 됩니다.
Docker docs
# Uninstall all conflicting packages
for pkg in docker.io docker-doc docker-compose 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
# Install the latest version
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
도커 설치를 마치고 도커 명령어를 실행했을 때,
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json": dial unix /var/run/docker.sock: connect: permission denied
이와 같은 오류가 발생한다면 도커 그룹에 USER를 추가해줍시다.
sudo usermod -aG docker USERNAME
USERNAME 부분에 현재 사용자의 이름을 넣고 실행하면 됩니다.
재접속 후, docker ps
명령어를 사용하면 다음과 같은 결과가 나올 것입니다.
도커 설치를 마쳤습니다.
도커 설치를 마쳤다면, Jenkins 이미지를 도커허브로부터 내려 받고, 해당 이미지를 컨테이너로 실행시켜 봅시다.
docker pull jenkins/jenkins:lts
docker run --privileged -d -p 8080:8080 -p 50000:50000 --name jenkins jenkins/jenkins:lts
도커 컨테이너는 기본적으로 Unprivileged 모드로 실행되어, 시스템 주요 자원에 접근할 수 있는 권한이 부족합니다. 따라서 --privileged
옵션을 사용해 Privileged 모드로 실행하겠습니다. 자세한 내용은 아래 링크에서 확인하실 수 있습니다.
Docker docs - Runtime privilege
또한, Jenkins의 기본 포트인 8080으로 접속하기 위해 컨테이너 포트를 8080으로 실행시켜줍니다.
도커 컨테이너를 실행시켰다면, docker ps
명령어를 통해 jenkins가 실행 중인 것을 확인할 수 있습니다.
컨테이너가 실행되었다면, http://외부IP주소:8080
으로 Jenkins에 접속해줍시다.
위 사진과 같이 초기 비밀번호를 입력하는 창이 나올 것입니다. 이를 확인하기 위해 다시 GCP shell 화면으로 돌아가서 Jenkins 컨테이너에 접속해봅시다. 아래 명령어를 통해 컨테이너에 접속합니다.
docker exec -it jenkins /bin/bash
컨테이너에 접속했다면, docker container 내부 쉘에서 다음과 같이 초기 비밀번호를 확인해줍니다.
cat /var/jenkins_home/secrets/initialAdminPassword
내용을 복사해 넣어주고, Continue를 선택합니다.
그럼 위와 같이 어떤 방식으로 플러그인을 설치할 것인지 선택하는 창이 나옵니다. Install suggested plugins을 선택해 필수적인 플러그인을 모두 설치받도록 합시다.
설치가 완료되면, Admin 계정을 생성하는 페이지가 나오게 됩니다. 여기서 설정한 계정을 통해 앞으로 Jenkins 에 접속하게 될 것입니다. 계정을 생성하고 Save and Continue를 선택하면 기본 URL을 설정하는 화면이 나오는데 외부IP주소:8080
으로 설정되어 있을 것입니다. 앞으로 이 URL을 통해 Jenkins를 접속하게 될 것입니다. 변경하지 말고 Sava and Finish를 누르면 Jenkins 메인페이지가 나오게 됩니다.
여기까지 Jenkins를 설치하고 접속까지 완료했습니다.
젠킨스 메인페이지에서 ✚ 새로운 Item
을 선택하고 item 이름을 설정하고 Freestyle Project
를 생성합니다.
item 이름에 공백을 넣지 맙시다. 이것 때문에 꽤나 고생했습니다..ㅠㅠ
General 탭에서 Github project
를 체크해 활성화 시켜줍니다. 그리고 연동하고자 하는 Github repository의 URL을 입력합니다.
다음으로 소스 코드 관리 탭에서 Git을 선택하고 내용을 입력합니다.
Repository URL은 위와 같은 주소이고, Credentials를 설정해줄껀데, 이는 Jenkins와 Github 사이에 데이터를 주고 받을 때의 인증 방식을 의미합니다. SSH-key 인증 방식을 많이 이용하지만, 테스트를 위해 Github 계정 인증 방식을 사용하겠습니다. 추후에 변경해주면 됩니다!
아래의 Add를 클릭하고 Jenkins를 선택하면 다음과 같은 창이 뜹니다.
이곳에 자신의 Github 계정을 입력해주면 됩니다. 입력을 마치고 Add를 통해 등록했다면, Credentials이 -none-
으로 되어 있는 것을 설정한 계정으로 바꾸어주면 됩니다.
Branches to build에서 Github에 push 가 될 때 build가 실행될 브랜치를 선택할 수 있습니다. 현재 제 repository의 default branch는 main branch 이기 때문에 main으로 선택하겠습니다.
다음으로 빌드 유발 탭에서 아래와 같이 체크합니다.
그 다음으로 Build Steps 탭에서 어떤 방식으로 빌드를 수행할지 설정할 수 있는데 shell을 통한 방법을 선택하겠습니다.
shell 안에 다음과 같이 빌드할 내용을 지정합니다.
chmod +x gradlew
./gradlew clean build
‼️ 주의
현재 제 repository의 폴더 구조는 다음과 같습니다.
상위 폴더 없이 바로 노출되어 있는 구조입니다.
만약 상위 폴더 안에 프로젝트 파일이 존재한다면, 위 명령어를 수행하기 전에cd ...
명령어를 통해 프로젝트로 들어간 후 위 명령어를 작성하면 됩니다.
예시)cd project_name chmod +x gradlew ./gradlew clean build
다음으로 연동하고자 했던 repository에 접속해 Webhook을 설정합시다. 해당 repository의 Settings → Webhooks
로 들어갑니다.
Add webhook으로 webhook을 추가합시다. 구성 내용은 아래와 같이 설정합니다.
http://123.45.678:8080/github-webhook/
(맨 뒤의 /를 꼭 붙여야 합니다!)작성을 마쳤다면, Add webhook을 통해 생성합니다.
만약 앞서 Jenkins 설정 시 Credentials를 Github 로그인 방식이 아닌 SSH-key 방식을 사용했다면, github Deploy 란에서도 shh-key를 등록해야 연동이 가능해집니다.
다음으로, springboot 프로젝트가 올라가는 VM 인스턴스(서버)와 연동하는 방법에 대해 알아봅시다!
간략하게 과정은 다음과 같습니다.
Jenkins 서버와 springboot 서버를 연동하기 위해, PEM 형식의 key를 생성합니다.
만약 아래 방법으로 연동 시 BapPublisherException와 같은 오류가 발생한다면, 아래 블로그 링크를 통해 해결해보시기 바랍니다.
간단히 말하자면, OpenSSH 8.8부터 SHA-1 해시 알고리즘을 사용하는 RSA 시그니처를 지원하지 않기로 결정해 ECDSA를 사용해야 한다고 하네요.
Jenkins Publish over SSH 인증시 BapPublisherException 오류
Jenkins 서버에서 아래와 같이 ssh-key를 생성합니다.
ssh-keygen -t rsa -C "key_name" -m PEM -f ~/.ssh/"key_name"
예시) ssh-keygen -t rsa -C "id_rsa" -m PEM -f ~/.ssh/id_rsa
그리고 엔터 두 번을 입력해주시면 ssh-key 생성이 완료됩니다.
앞서 생성한 key 중 public key를 springboot 배포 서버에 추가하는 작업을 해봅시다.
GCP에서는 아래와 같이 메타데이터에 들어가 SSH 키를 등록해주면 됩니다.
메타데이터 창에 들어가 SSH키 탭을 누르고 상단의 수정 버튼을 눌러 추가합니다.
이때, 넣을 key는 public key로, 위에서 생성한 key 중 .pub
가 붙은 key를 복사해 추가하면됩니다.
아래 코드에서 확인할 수 있습니다.
cat ~/.ssh/id_rsa.pub
만약 위 방법으로 연동이 되지 않는다면, springboot 배포 서버에 접속한 뒤,
.ssh
폴더로 들어가면
authorized_keys
파일이 있을 것입니다. 이곳에 vi를 통해 붙여 넣어주시면 됩니다.
참고) GCP에서는 메타데이터에 SSH Key를 추가하면 자동으로authorized_keys
파일에 추가가 됩니다.
참고로, springboot 배포를 위한 VM 인스터스는 위의 Jenkins 서버와 같은 방식으로 생성했습니다. 똑같은 Ubuntu 환경입니다.
또한, 배포 서버는 권한 문제로 root 환경으로 모든 작업을 수행했습니다. 아래 방법으로 root 계정으로 접속할 수 있습니다.
# root 비밀번호 설정하기
sudo passwd
# root로 접속하기
su -
springboot 배포 서버에서 DockerHub를 통해 이미지를 실행시켜야하기 때문에 Jenkins 서버와 같은 방식으로 Docker를 설치해 줍시다.
위의 과정을 모두 마쳤다면, Jenkins의 Publish Over SSH 플러그인을 통해 두 인스턴스를 연동해봅시다.
우선 플러그인 설치를 위해, Jenkins 메인화면에서 Jenkins 관리 → System Configuration의 Plugins → Available plugins 탭에 들어가 검색창에 "ssh"를 입력하고 Publish Over SSH
를 선택해 설치해줍니다.
설치했다면, 맨 아래 "설치가 끝나고 실행중인 작업이 없으면 Jenkins 재시작."를 체크하여 Jenkins를 재시작해줍니다.
제가 여러 번 실행해본 결과, 이를 통해 재시작을 하면 docker로 실행한 jenkins가 종료됩니다. 따라서, Jenkins 서버에 접속해 아래 명령어를 통해 다시 jenkins를 실행시켜 주면 됩니다.
docker start jenkins
플러그인 설치를 마쳤다면 Jenkins 관리 → System Configuration의 System 에 들어가 아래의 Publish over SSH에서 연동 작업을 진행합시다.
Key 부분에 앞서 생성한 SSH key의 private key를 넣어주시면 됩니다.
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
이때, 위처럼 모든 부분을 넣어야 합니다.
그리고 바로 아래의 SSH Servers 추가를 선택합니다. 그럼 다음과 같은 창이 뜨게 됩니다.
위의 내용을 모두 작성하고 Test Configuration을 눌렀을 때, 다음과 같이 Success가 나온다면 연동이 완료된 것입니다.
Jenkins에서 도커 이미지를 build 하기 위해, Jenkins 컨테이너 안에 Docker를 설치하는 과정이 필요합니다. 이를 도커 안에 도커를 설치한다고 하여, Docker in Docker 즉, DinD라 합니다.
도커 측에서는 DinD 방식보다 DooD(Docker out of Docker) 방식을 더 권장하지만, 여기서는 DinD 방식을 사용하겠습니다.
우선, Jenkins 서버에서 Jenkins 컨테이너를 root로 접속합니다.
docker exec -itu 0 jenkins /bin/bash
그리고 아래 코드를 통해 Docker를 설치해줍시다.
# Docker 설치
## - Old Version Remove
apt-get remove docker docker-engine docker.io containerd runc
## - Setup Repo
apt-get update
apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release
mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
## - Install Docker Engine
apt-get update
apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
설치가 완료됐다면, 아래 명령어로 도커데몬을 실행시켜주면 됩니다.
service docker start
usermod -aG docker root
su - root
id -nG
# "root docker"가 뜨면 정상
chmod 666 /var/run/docker.sock
Jenkins 에서 DockerHub에 빌드된 도커 이미지를 push 할 수 있도록, root로 접속해 도커 로그인을 합시다. 이때, 아이디와 비밀번호는 DockerHub의 아이디와 비밀번호를 입력하면 됩니다.
su - root
docker login
결과로, "Login Succeeded"가 뜨면 정상적으로 로그인이 된 것입니다.
드디어 마지막 단계입니다!
Jenkins 메인 화면에서 생성했던, Item(Freestyle Project)에 들어갑니다. 접속한 뒤, 왼쪽의 구성 탭을 클릭합니다.
이전에 작성해두었던 Execute shell 아래에 Add build step을 선택하고, Execute shell을 하나 더 추가한 뒤, 아래 코드를 작성합시다.
docker login -u '도커허브아아디' -p '도커허브비번' docker.io
docker build -t [dockerHub UserName]/[dockerHub Repository] .
docker push [dockerHub UserName]/[dockerHub Repository]
build 명령어 수행 시, 마지막에 "."을 꼭 붙여주셔야 합니다!
이 과정을 수행하면 Jenkins에서 Docker image를 만들어 DockerHub에 push 하는 과정이 끝이 난 것입니다.
이제 springboot 배포 서버에서 이를 pull 받아 실행시키는 작업을 해봅시다.
그 바로 아래, 빌드 후 조치 탭에서 Send build artifacts over SSH를 선택해줍니다. 그리고 다음과 같이 작성합시다.
모든 작성이 완료되었다면 저장하고 지금 빌드 탭을 선택해 빌드를 시작해봅시다!
왼쪽의 빌드 결과에 들어가 Console Output 을 선택해 콘솔 결과를 확인해보면,
빌드가 성공적으로 수행된 것을 확인할 수 있고
결과적으로 모든 과정이 성공적으로 진행된 것을 확인할 수 있습니다!
배포를 테스트하기 위해, springboot 프로젝트에 다음과 같은 클래스를 만들어 main branch에 push를 진행해 보겠습니다.
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/hello")
public HelloResponse getHello(){
return new HelloResponse(1L, "Hello World New Version");
}
@Data
@AllArgsConstructor
@NoArgsConstructor
static class HelloResponse{
private Long id;
private String content;
}
}
이제 springboot 배포 서버의 외부IP주소를 이용해, http://외부IP주소:8080/api/test/hello
에 접속해보면!!
다음과 같이 정상적으로 결과가 뜨는 것을 확인할 수 있습니다..!
지금까지 Jenkins + Docker를 이용한 CI/CD 구축에 대해 알아보았습니다.
프로젝트를 수행하며 Jenkins를 사용해보고 싶다는 마음에 무턱대고 시도했지만, 역시나 한 번에 성공하는 것은 없더라구요. 수많은 오류를 만나고(발생할 수 있는 오류란 오류는 모두 만났던 것 같네요...) 수많은 구글링을 거치며 좌절도 했지만, 결국 해냈을 때의 쾌감은 ㅠㅠ.. 말로 설명할 수 없었습니다.
혹시 구글링 도중 이 포스트를 발견해 참고하시는 분들은 부디 무탈히 구축하셨으면 하는 바람입니다. 궁금한 점이나 잘못된 부분이 있다면 언제든 말씀해주세요. 감사합니다 :)
Docker 설치
Docker push 에러
Jenkins 서버 연동 오류
Jenkins를 이용한 Docker 기반 스프링부트 배포 자동화
[Jenkins] GCP + Docker + Jenkins를 이용한 CI / CD