[CI/CD] Jenkins + Docker를 이용한 GCP 환경 Springboot 애플리케이션 배포 자동화

들어가며

새로운 프로젝트를 진행하며 서버 배포 자동화를 위해 Jenkins를 도입하기로 결정했습니다. Jenkins를 이용한 배포 자동화 과정을 공유하고자 합니다. 

 

왜 Jenkins를 사용했을까?

 이전 다수의 프로젝트 서버 배포는 로컬에서 개발한 springboot 프로젝트를 수동으로 빌드하는 방식을 사용했었습니다. 따라서, 프로젝트에서 수정사항이 생길 때마다 매번 재빌드해 배포해야하는 번거로움이 있었습니다. 이는 정말 불편하게 느껴집니다. 따라서, 배포 자동화에 관심이 생겨 공부를 진행하다 CI/CD 를 접하게 되었고, 이번 프로젝트에 적용하기로 결정했습니다. 사실 한 번 써보고 싶었던게 제일 클지도...

 

프로젝트 구조

저희 프로젝트의 서버 아키텍처는 다음과 같이 배포 자동화 시스템을 구축했습니다.

 

Jenkins와 Docker 를 이용해 배포가 자동화되는 과정을 간단히 살펴보자면, 다음과 같습니다.

  1. 로컬에서 작업한 내용을 Jenkins와 연동된 Github 레포지토리에 push 합니다.
  2. push된 내용은 Webhook을 통해 Jenkins에 전달되고 Jenkins 서버에서 Gradle을 통해 Build를 실행합니다.
  3. gradlew build 를 통해 Jar 파일이 생성되고 이를 기반으로 도커 이미지(Docker Image)를 build 합니다.
  4. build된 도커 이미지가 개인 DockerHub에 push 됩니다.
  5. Springboot 프로젝트를 배포할 인스턴스(서버)에서 DockerHub에 올라간 도커 이미지를 pull 받습니다.
  6. pull 받은 도커 이미지를 기반으로 도커 컨테이너를 실행시킵니다.

📖 Dockerfile 작성

우선, 개발을 진행할 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 내용에 대해서는 여기서 자세히 다루지 않겠습니다. 자세히 알고 싶다면 아래 블로그를 참고하면 좋을 것 같습니다:)

도커파일 작성법

 

docker :: 도커파일(Dockerfile) 의 개념, 작성 방법/문법, 작성 예시

1. 도커파일(Dockerfile) 이란? 도커파일은 docker 에서 이미지를 생성하기 위한 용도로 작성하는 파일이다. 만들 이미지에 대한 정보를 기술해 둔 템플릿(template) 이라고 보면 된다. 도커 이미지를 만

toramko.tistory.com

 

# 🛠️ Jenkins 서버 환경 구축

저는 GCP 환경에서 배포를 진행하기 때문에 GCP를 기준으로 설명하겠습니다. 우선 Jenkins 서버를 위한 인스턴스를 하나 생성하고 Docker를 통해 Jenkins를 빌드하겠습니다. 

 

GCP → Compute Engine → VM 인스턴스로 들어가 상단의 `인스턴스 만들기`로 Jenkins를 위한 인스턴스를 생성합시다. 저는 부팅 디스크의 운영체제를 Ubuntu로 선택했습니다.

또한, 방화벽 탭에서 HTTP, HTTPS 트래픽을 모두 허용시켜 줍니다. 
생성하고, 접속해봅시다!

Docker 설치하기

Jenkins를 Docker를 통해 실행하기 위해, 우선 Docker를 설치해줍시다. 
저는 Ubuntu 환경에서 진행했기 때문에 아래와 같이 설치하겠습니다. 
다른 운영체제를 사용 중이라면, 아래 docker docs에서 자신에게 맞는 운영체제를 선택해 따라 설치해주면 됩니다. 

Docker docs

 

Install Docker Engine

Learn how to choose the best method for you to install Docker Engine. This client-server application is available on Linux, Mac, Windows, and as a static binary.

docs.docker.com

 

# 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 Image Pull + Run

도커 설치를 마쳤다면, 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

 

Running containers

Running and configuring containers with the Docker CLI

docs.docker.com

또한, Jenkins의 기본 포트인 8080으로 접속하기 위해 컨테이너 포트를 8080으로 실행시켜줍니다.

도커 컨테이너를 실행시켰다면, `docker ps` 명령어를 통해 jenkins가 실행 중인 것을 확인할 수 있습니다. 

 

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를 설치하고 접속까지 완료했습니다. 

 

🛠️ CI 구축: Github + 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 - Execute shell

그 다음으로 Build Steps 탭에서 어떤 방식으로 빌드를 수행할지 설정할 수 있는데 shell을 통한 방법을 선택하겠습니다. 

shell 안에 다음과 같이 빌드할 내용을 지정합니다. 

chmod +x gradlew
./gradlew clean build


!! 주의

현재 제 repository의 폴더 구조는 다음과 같습니다. 

상위 폴더 없이 바로 노출되어 있는 구조입니다. 
만약 상위 폴더 안에 프로젝트 파일이 존재한다면, 위 명령어를 수행하기 전에 `cd ...` 명령어를 통해 프로젝트로 들어간 후 위 명령어를 작성하면 됩니다. 
예시)

cd project_name
chmod +x gradlew
./gradlew clean build

🔗 Webhook 연동

다음으로 연동하고자 했던 repository에 접속해 Webhook을 설정합시다. 해당 repository의 `Settings → Webhooks`로 들어갑니다. 

Add webhook으로 webhook을 추가합시다. 구성 내용은 아래와 같이 설정합니다. 

  • Payload URL = Jenkins IP주소:8080/github-webhook/
    예시) `http://123.45.678:8080/github-webhook/` (맨 뒤의 /를 꼭 붙여야 합니다!)
  • Content type : application/json

작성을 마쳤다면, Add webhook을 통해 생성합니다.

만약 앞서 Jenkins 설정 시 Credentials를 Github 로그인 방식이 아닌 SSH-key 방식을 사용했다면, github Deploy 란에서도 shh-key를 등록해야 연동이 가능해집니다.

 

🔌 Jenkins와 Springboot 배포 서버 연동

다음으로, springboot 프로젝트가 올라가는 VM 인스턴스(서버)와 연동하는 방법에 대해 알아봅시다! 
간략하게 과정은 다음과 같습니다.

  • Jenkins 서버에서 빌드한 JAR 파일을 통해 생성된 도커 이미지(Docker image)가 DockerHub에 push 되고, 이 이미지를 springboot 서버에서 pull 을 받아 실행시킵니다.
  • 때문에, jenkins 서버와 springboot 서버가 연동되어야 하고 이를 SSH-key 방식을 이용해 진행하겠습니다. 

SSH key 생성하기

Jenkins 서버와 springboot 서버를 연동하기 위해, PEM 형식의 key를 생성합니다. 

만약 아래 방법으로 연동 시 BapPublisherException와 같은 오류가 발생한다면, 아래 블로그 링크를 통해 해결해보시기 바랍니다. 간단히 말하자면, OpenSSH 8.8부터 SHA-1 해시 알고리즘을 사용하는 RSA 시그니처를 지원하지 않기로 결정해 ECDSA를 사용해야 한다고 하네요.
Jenkins Publish over SSH 인증시 BapPublisherException 오류
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 생성이 완료됩니다. 

 

springboot 서버 public 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를 설치해 줍시다. 

 

Publish Over SSH 플러그인 사용하기

위의 과정을 모두 마쳤다면, 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 추가를 선택합니다. 그럼 다음과 같은 창이 뜨게 됩니다. 

  • Name : 사용할 서버 이름을 넣어주시면 됩니다. 
  • Hostname : springboot 배포 서버의 내부 IP 주소입니다.
  • Username : 사용자 이름이 들어가는 부분입니다. 저희는 root에서 작업하기로 했으므로 여기서는 root가 될 것입니다. 

위의 내용을 모두 작성하고 Test Configuration을 눌렀을 때, 다음과 같이 Success가 나온다면 연동이 완료된 것입니다. 

 

🐳 DinD(Docker in Docker) : Jenkins 컨테이너에 도커 설치 + 권한 설정

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

 

Jenkins & Docker 그룹 추가

1. Docker 그룹에 root 계정 추가

usermod -aG docker root
su - root

2. 추가되었는지 확인

id -nG
# "root docker"가 뜨면 정상

3. docker.sock 권한 변경

chmod 666 /var/run/docker.sock

4. root 에서 Docker 로그인

Jenkins 에서 DockerHub에 빌드된 도커 이미지를 push 할 수 있도록, root로 접속해 도커 로그인을 합시다. 이때, 아이디와 비밀번호는 DockerHub의 아이디와 비밀번호를 입력하면 됩니다. 

su - root
docker login

결과로, "Login Succeeded"가 뜨면 정상적으로 로그인이 된 것입니다. 

 

💡 Docker를 활용해 Jenkins 서버에서 Springboot 서버로 배포 자동화하기 

드디어 마지막 단계입니다! 
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를 선택해줍니다. 그리고 다음과 같이 작성합시다.

모든 작성이 완료되었다면 저장하고 지금 빌드 탭을 선택해 빌드를 시작해봅시다!

 

🤣 Build 및 배포 테스트

왼쪽의 빌드 결과에 들어가 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 설치

 

Install Docker Engine

Learn how to choose the best method for you to install Docker Engine. This client-server application is available on Linux, Mac, Windows, and as a static binary.

docs.docker.com

Docker push 에러

 

[docker] push 에러 (An image does not exist locally with the tag: 1013cm/coupang_api)

docker push 에러 docker push 1013cm/coupang_api 위의 명령어로 도커 이미지를 push 하려고 하는데, 아래와 같은 오류가 발생했습니다. (참고로 위의 명령어는 도커 허브에 있는 제 repository인 1013cm/coupang_api

taewooblog.tistory.com

Jenkins 서버 연동 오류

 

jenkins server 연동 (오류 해결)

jenkins server 연동 Publish over SSH jenkins서버에서 id_rsa 키값 발급받아서 키 넣어두고 ( 발급 방법 구글에 찾아보자 ) ssh Server설정 위 내용 추가 remote서버 root에서 ~/.ssh/authorized_keys 에 id_rsa.pub 키 등록

genie247.tistory.com

[Jenkins] GCP + Docker + Jenkins를 이용한 CI / CD

 

[Jenkins] GCP + Docker + Jenkins를 이용한 CI / CD

Index Chapter 1 Chapter 2 젠킨스 서버에 도커 및 젠킨스 설치, 배포 서버에 도커 설치 젠킨스 설치 후 이것저것 설정 젠킨스 Job 생성 Chapter 3 배포 서버에 도커파일 생성 배포 서버에 init.sh 작성 깃헙에

dev-gorany.tistory.com