[Jenkins] Docker를 활용한 Spring boot 프로젝트 CI & CD

2020. 7. 26. 20:19카테고리 없음

혼자 생각한대로 배포를 진행해봤는데, 이렇게 하는게 맞는지 아직 잘 모르겠다. 포스팅에 문제점이나 잘못된 점이 있다면 피드백을 받을 수 있으면 좋겠다.

Docker 설치

먼저, AWS ec2 인스턴스를 하나 준비하고, 터미널에서 apt-get을 업데이트 해준 뒤, 도커를 받는다.

$ sudo apt-get update
$ sudo apt-get -y upgrade
$ curl -fsSL https://get.docker.com/ | sudo sh

...

$ docker --version

도커가 깔린 것을 확인할 수 있다.

위 명령으로 설치한 도커는 모두 root 권한으로 실행해야한다. 따라서, 현재 유저에게 docker를 사용할 수 있는 권한을 주기 위해 다음 커맨드를 실행한다.

$ sudo usermod -aG docker $USER
$ sudo service docker restart

# 아래는 재로그인(연결을 종료했다 다시 연결해도 된다)
$ sudo su
$ sudo su ubuntu

$ docker ps

현재 유저가 docker 명령어를 사용할 수 있는 권한은 얻었다.

다음은 docker-compose를 설치해야한다. 중간에 있는 최신 버전은 여기서 확인할 수 있다. 설치 후에는 권한을 주기 위해서 chmod 명령어를 사용한다.(가끔 설치 명령어를 복붙하면 중간에 \ 가 들어가는 경우가 있다. 에러가 난다면 $\(uname -s) 와 같이 \가 있는지 확인해보자)

$ sudo curl -L https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose

$ sudo chmod +x /usr/local/bin/docker-compose

$ docker-compose --version

docker-compose가 깔린 것을 확인한다.

Spring boot 프로젝트 Github 업로드

간단하게 "Hello, Jenkins!"를 출력해주는 Spring boot 프로젝트를 Github 저장소에 업로드한다(예제 링크)

여기까지의 진행과정을 그림으로 표현하면 다음과 같다.

젠킨스 설치

$ mkdir compose && cd compose
$ mkdir jenkins-dockerfile && cd jenkins-dockerfile
$ vim Dockerfile

...
FROM jenkins/jenkins:lts

USER root
RUN apt-get update &&\
    apt-get upgrade -y &&\
    apt-get install -y openssh-client
...

$ cd ..
$ vim docker-compose.yml

...
version: "3"
services:
  jenkins:
    container_name: jenkins-compose
    build:
      context: jenkins-dockerfile
      dockerfile: Dockerfile
    user: root
    ports:
      - 8000:8080
      - 8888:50000
    volumes:
      - /home/ubuntu/compose/jenkins:/var/jenkins_home
      - /home/ubuntu/compose/.ssh:/root/.ssh
...

$ mkdir jenkins
$ mkdir .ssh

Dockerfile의 내용을 간단하게 설명하면 다음과 같다.

  • jenkins/jenkins 이미지의 lts 버전을 docker-hub로부터 받아와서 사용한다.
  • root 유저의 권한으로 openssh-client를 설치한다.

docker-compose.yml의 내용은 다음과 같다.

  • jenkins라는 이름의 서비스를 만든다.
  • 컨테이너의 이름은 jenkins-compose이다.
  • ./jenkins-dockerfile 경로에 있는 Dockerfile을 빌드한다.
  • 접속 유저를 root로 한다.
  • 포트는 ec2 인스턴스의 8000, 8888번 포트를 도커 컨테이너의 8080, 50000번 포트에 대응시킨다.
  • 공유할 volumn을 지정한다. "ec2 인스턴스의 경로 : 도커 컨테이너의 경로"와 같은 식으로 지정하면 각 경로의 디렉토리를 공유한다.

여기까지 완료되면 ec2 인스턴스의 디렉토리 구조는 다음과 같을 것이다.

ec2 디렉토리 구조

이제 이 docker-compose 파일을 빌드하고 실행하자. docker-compose up 명령어는 이미지를 빌드하고, 컨테이너를 실행시켜주는 명령어이다.

$ docker-compose up --build -d

...

$ docker image ls
$ docker ps

빌드가 성공적으로 됐다면 위 화면을 볼 수 있다.

docker image ls 명령어는 이미지들을 나열하는 명령어이다.
그림에서 jenkins/jenkins:lts 이미지는 Dockerfile의 명령어로 Docker Hub로부터 pull 받아진 이미지이다. compose_jenkins:latest 이미지는 위의 docker-compose.yml으로부터 직접 빌드된 이미지이다.

docker ps 명령어는 현재 실행중인 컨테이너를 나열하는 명령어이다.

jenkins-compose란 이름의 컨테이너가 실행중임을 알 수 있다.

이미지를 받아올 때 주의할 점은 jenkins:lts가 아닌 jenkins/jenkins:lts를 받아와야 하는 점이다. jenkins:lts는 관리가 안되는 옛날 버전이라 진행이 불가능하다.

젠킨스 설정

젠킨스 설정을 위해 http://[ec2-주소-입력]:8000 으로 접속해보자. 아래와 같이 비밀번호를 입력하라는 창이 나온다.

$ docker logs jenkins-compose

현재 실행중인 jenkins-compose란 이름의 컨테이너의 로그를 출력한다. 우리가 받아온 jenkins/jenkins:lts 이미지는 비밀번호를 로그로 남기게 되어이씩 때문에 화면에 비밀번호가 출력 될 것이다.

비밀번호가 출력된다.

이 비밀번호를 붙여넣으면 된다. 다음 화면에서는 suggested plugin을 설치해준다.

플러그인 설치

플러그인 설치가 모두 완료됐다면 적당히 칸을 채워주고 넘어가면 된다.

Admin 계정 생성
URL 설정

계정 설정이 끝났다면 플러그인 관리로 들어간다.

플러그인 관리 접속

github으로부터 저장소의 변경 정보를 받아오기 위해 github integration을 설치해준다. 재시작이 안된다 싶으면 새로고침을 몇 번 누르다보면 된다.

github integration 플러그인 설치

다시 원래 페이지로 돌아와서 새로운 item을 생성한다. 아이템은 github의 액션에 반응해서 특정한 task를 수행하는 역할을 한게 된다.

item 생성 클릭

item의 이름을 입력하고 Freestyle project를 선택한다. 이름은 뒤에 폴더 이름이 되기 때문에 공백 같은 문자는 피하는 것이 좋다.

아이템 생성

다음 페이지의 소스 코드 관리에서 git을 선택한다. 앞에서 만든 Spring boot 프로젝트를 업로드한 github 저장소의 주소를 적고 유저 인증 정보를 추가하기 위해 아래의 `Add` 버튼을 클릭한다.

github 저장소 등록

github 아이디와 비밀번호를 입력한다(보안상 권장되는 방법은 아니다. 여기서는 간단하게 이렇게 진행하겠다).

github 계정 정보 입력
계정 선택
계정이 선택된 것을 확인한다.

github의 webhook이 빌드를 유발하도록 허용하고, 빌드 동작을 shell 명령어를 실행하도록 한다.

CI를 위해 "./gradlew clean build" 명령을 실행하도록 한다. 다음, 설정을 저장한다.

위 설정 과정을 요약하면 다음과 같다.

  1. 테스트 및 빌드를 수행할 github repository를 등록한다.(계정 정보도 같이 등록한다)
  2. 등록한 저장소에서 webhook(이벤트)이 들어올 때, shell command로 ./gradlew clean build를 실행하도록 한다.

이제 jenkins 서버는 github 저장소에서 webhook이 들어오기를 기다리고 있다. 반대로, github 저장소에서도 변경사항이 일어나면 jenkins 서버로 명령을 전송하는 설정을 해야한다. 아까 생성한 저장소로 이동해서 webhook setting을 하자.

webhook 설정

payload URL에 http://[ec2-주소]:8000/github-webhook/ 를 입력한다.

포트번호와 마지막 / 를 꼭 입력해야한다!

Jenkins 컨테이너 주소 입력

아래 초록색 체크 표시가 뜨면 정상적으로 github 저장소에 Jenkins 서버가 등록된 것이다.

이제 github 저장소에 아무 내용이나 push 한 뒤에, Jenkins에서 테스트가 실행되는지 확인해보자.

README.md 등을 변경한 뒤, commit & push를 해보자.

빌드가 진행되고 있다.

해당 빌드를 눌러서 클릭해서 들어간 뒤, console output를 확인해보면 진행 상황을 볼 수 있다.

README.md를 업데이트한 commit인 "Update README.md"에 대하여 아까 설정해뒀던 shell 명령어인 "./gradlew clean build"가 실행되었음을 확인할 수 있다.

현재까지의 과정을 그림으로 표현하면 다음과 같다.

Spring Boot 컨테이너 생성

이제 Spring Boot 서버를 올릴 컨테이너를 생성해보자.

먼저 아까 jenkins를 통해 빌드한 파일을 찾아보자. 아까 docker-compose.yml을 다시 살펴보면

version: "3"
services:
  jenkins:
    volumes:
      - /home/ubuntu/compose/jenkins:/var/jenkins_home

ec2 로컬의 ~/compose/jenkins와 jenkins 컨테이너의 /var/jenkins_home 경로는 파일을 공유함을 알 수 있다.
따라서 jenkins에서 ./gradlew clean build 명령을 통해 생성된 jar 파일을 ec2에서도 직접 접근할 수 있다.
github에서 clone한 프로젝트는 컨테이너 내부의 /var/jenkins_home/workspace/{jenkins item 이름}에 저장된다. 따라서 ec2 로컬에서 아래 명령어로 jar 파일의 이름을 확인할 수 있다.

$ ls ./jenkins/workspace/{jenkins item 이름}/build/libs

먼저 Spring Boot용 Dockerfile을 생성한다.

$ mkdir spring-dockerfile && cd spring-dockerfile
$ vim Dockerfile

...
FROM openjdk:8-jdk

ENTRYPOINT java -jar /deploy/hello-jenkins-0.0.1-SNAPSHOT.jar

EXPOSE 8080
...

위 Dockerfile은 jdk-8이 설치되어있는 이미지를 받고, deploy 폴더에 있는 jar 파일을 실행한다는 뜻이다.

ENTRYPOINT의 마지막 파일명에는 위에서 찾은 jar 파일명을 적으면 된다.

이 Dockerfile을 빌드하기 위해 docker-compose.yml 파일을 다음과 같이 수정한다.

version: "3"
services:
  jenkins:
    container_name: jenkins-compose
    build:
      context: jenkins-dockerfile
      dockerfile: Dockerfile
    user: root
    ports:
      - 8000:8080
      - 8888:50000
    volumes:
      - /home/ubuntu/compose/jenkins:/var/jenkins_home
      - /home/ubuntu/compose/.ssh:/root/.ssh
  spring:
    container_name: spring-compose
    build:
      context: spring-dockerfile
      dockerfile: Dockerfile
    ports:
      - 8080:8080
    volumes:
      - /home/ubuntu/compose/jenkins/workspace/spring-boot-ci-cd/build/libs:/deploy

현재까지 작성한 디렉토리 구조

 

jenkins 서비스는 그대로이고 spring 서비스를 추가했다. ec2 컨테이너의 빌드된 jar 파일이 있는 경로를 도커 컨테이너의 /deploy 경로와 공유한다. 여기까지 작성했으면 spring 서버를 띄울 수 있다.

$ docker-compose --build -d

$ docker ps

docker-compose up

아까 빌드했던 jenkins 이미지는 캐싱되어 있기 때문에 빠르게 지나가고, 한번도 빌드하지 않았던 spring 서비스는 pull부터 빌드과정을 거치게 된다.

docker ps 결과

jenkins 컨테이너와 spring 컨테이너가 실행중임을 확인할 수 있다. 이제 외부에서도 ec2 주소를 통해 접근할 수 있다.

현재까지의 과정을 그림으로 나타내면 다음과 같다.

자동 배포

현재 상태로는 jenkins에서 빌드된 파일이 자동으로 배포되지 않는다. 직접 명령어를 실행해야 하는데, 이를 jenkins의 빌드가 끝나면 자동으로 서버에 배포하도록 해보자. 원하는 시나리오는 아래와 같다.

jenkins 컨테이너에서 ec2 로컬로 배포 명령을 하기 위해서는 ssh를 이용해야한다. ssh 명령을 password 없이 수행하려면 client 쪽(도커 컨테이너) 공개키를 server(ec2 로컬) 쪽 허용키에 등록해줘야한다. 따라서, 공개키 생성을 위해 도커 컨테이너로 접속해야한다.

$ docker exec -it jenkins-compose bash

jenkins-compose란 이름의 컨테이너에서 bash를 실행했기 때문에 해당 컨테이너의 터미널이 나온다. 위와 같이 "root@~"로 시작하는 이름이 나오면 된다. 이제 키를 생성해보자. 중간에 몇가지 질문이 나오는데 모두 입력하지 않고 그냥 엔터키를 누르면 된다.

$ ssh-keygen -t rsa

$ cat /root/.ssh/id_rsa.pub

$ exit

제일 아래 키는 복사해둔다.

공개키는 ssh-rsa부터 뒤에 root@~ 까지 모두 복사해둔다. 키 생성이 모두 끝났으면 `exit` 명령어로 ec2 로컬로 돌아온다.

이제 생성한 jenkins 컨테이너의 키를 ec2 로컬 허용키 목록에 등록한다.

$ vim ~/.ssh/authorized_keys

파일에 아무 내용이 없다면 그냥 붙여넣기 하면 되고, 내용이 있다면 맨 마지막에 복사한 내용을 추가하자.
접속이 잘 되나 확인해보자. 다시 도커 컨테이너에 들어가서 ssh 명령을 날려본다.

$ docker exec -it jenkins-compose bash

$ ssh ubuntu@$(/sbin/ip route | awk '/default/ { print $3 }')

$(/sbin/ip route | awk '/default/ { print $3 }') 명령어는 도커 컨테이너 내부에서 ec2 로컬로 접속할 수 있는 주소를 출력한다. 

Ubuntu에 온걸 환영합니다!

위와 같이 ec2 로컬에 접속한 것을 환영한다는 문구가 나오면 된다. exit 명령어를 통해 다시 도커 컨테이너로 돌아오자.

$ exit

우리의 목적은 ssh로 접속하는 것이 아니라 명령을 내리는 것이다. ec2 로컬에 docker compose up 명령을 내리려면 다음과 같이 한 줄씩 입력하면 된다. (-t 옵션이 두번 들어가는 것에 유의해야한다)

$ ssh -t -t ubuntu@$(/sbin/ip route | awk '/default/ { print $3 }') <<EOF
> cd /home/ubuntu/compose
> docker-compose up --build -d
> exit
> EOF

도커 컨테이너에서 위와 같이 명령을 보내면 ec2에서 `docker-compose up --build -d` 명령을 실행하게 된다. 다시 ec2 로컬로 돌아와서 실행중인 도커 컨테이너를 확인해보자.

jenkins와 spring 컨테이너가 모두 잘 돌아가는 것을 확인할 수 있다.

마지막으로, 도커 컨테이너에서 ec2 로컬로 명령을 보내는 방법을 알았으니 이를 jenkins에 적용만 하면 된다.

jenkins 아이템으로 돌아와서 "구성"을 클릭하고 script에 아까 성공했던 ssh 명령을 적는다.

여기까지 했으면 다시 빌드를 해보고, 명령이 잘 출력되는지 확인해본다.

지정해두었던 명령이 실행되는 것을 확인할 수 있다.

여기까지 docker를 활용한 jenkins CI & CD가 모두 끝났다. 하지만 여전히 몇가지 문제점이 남아있다. 폴더명, 파일명이 변하면 CI & CD 과정이 깨질 것이다. 또, 도커 캐싱으로 인해 변경사항이 서버에 적용되지 않는 문제점 등이 있다. 다음 포스팅에서 이를 다뤄볼 예정이다.

후기

배포를 처음해보는 거라 마음대로 환경을 구성했다. 젠킨스 서버와 백엔드 서버를 같이 둔 것에 대해서 의문을 가질 수도 있을 것 같다. 그럼에도 불구하고 둘을 같이 둔 것은 1. 이렇게 구성한 예시를 찾아볼 수가 없어서 한 번 해보고 싶었고, 2. 이번에 진행하는 프로젝트에서 할당받을 수 있는 서버의 댓수가 한 개밖에 없을 것 같아서였다. 만약 이 글을 끝까지 보신 분이 계시다면 이상한 부분에 대해 피드백을 주면 정말 감사할 것 같다.