1. Jenkins CI / CD 구축하기 - 스프링 Boot EC2 배포하기 with Docker
CI / CD를 구축하기 이전에 수동으로 도커이미지를 빌드하고 배포해보는 과정을 통해 배포의 전반적인 과정의 이해를 하는것이 매우 중요하다고 생각합니다. 단순히 블로그와 지피티를 통해서 코드를 복사 붙여넣기 하는것으로도 파이프라인을 구축할수는 있겠지만, 추후 트러블 슈팅이나 시스템 고도화과정에서 기본적인 배포에대한 이해가 없으면 해결하기 힘든 문제들에 직면할것입니다.
저 또한 수동배포를 한번 진행해본것이 도커에 대한 기본적인 이해와 CI / CD의 이해를 높이는데 많은 도움이 되었기에 먼저 수동으로 배포를 진행해보려고 합니다.
0. Docker란?
도커의 정의는 컨테이너 기반으로하는 오픈소스 가상화 플랫폼입니다.
흔히 생각하는 실제 컨테이너는 물건을 하나의 단위로 모아서 안전하게 운반하는 도구입니다. 컨테이너 안에는 다양한 물건이 들어있을 수 있지만, 그 안에 무엇이 있는지에 관계없이 컨테이너는 동일한 모양과 크기를 가집니다. 즉, 컨테이너 자체는 내용물과는 별개로 표준화되어 있어 어디서나 동일한 방식으로 운송 처리를 할 수 있게됩니다.
이와 비슷하게, Docker 컨테이너역시 애플리케이션을 실행하는 데 필요한 모든 것을 하나의 단위로 묶는 포장 도구입니다. 애플리케이션, 라이브러리, 의존성 등 필요한 모든 파일을 하나의 패키지에 담습니다. 이 컨테이너는 어떤 환경에서 실행되더라도 동일하게 작동할 수 있도록 표준화되어 있습니다. 따라서, 개발 환경, 테스트 환경, 배포 환경 등 서로 다른 서버에서도 동일한 애플리케이션을 일관되게 실행할 수 있습니다.
이러한 도커의 특성은 다음과 같은 장점을 가집니다.
1. 개발 환경의 일관성 보장:
개발자 A는 Windows 운영체제를, 개발자 B는 MacOS를, 서버는 Linux를 사용합니다. 이 경우, 개발 중 사용하는 라이브러리나 의존성 차이로 인해 환경에 따라 애플리케이션이 다르게 동작할 수 있습니다. 그러나 Docker를 사용하면 동일한 컨테이너를 로컬, 테스트 서버, 운영 서버에서 모두 실행할 수 있으므로 환경 차이로 인한 문제를 제거할 수 있습니다.
2. 빠른 배포 및 복구:
예를 들어, 웹 애플리케이션을 Docker로 패키징한 후 컨테이너로 배포하게 되면, 새로운 서버에 Docker만 설치하면 즉시 동일한 환경을 구성할 수 있습니다. 만약 서버에 문제가 생겨도, 같은 컨테이너 이미지를 사용해 빠르게 새로운 서버에 배포하고 복구할 수 있습니다.
이처럼 Docker는 개발부터 배포까지의 일관성, 이식성을 보장하면서도 효율적이고 격리된 환경을 제공해 다양한 환경에서 애플리케이션을 안정적으로 실행할 수 있게 합니다.
1. Spring 프로젝트 도커 빌드하기
기본적인 EC2 인스턴스 생성과 도커 설치가 되어있다고 가정하고 진행합니다. 다른 블로그들을 참고해서 사전 설정을 마친 뒤 진행해 주세요
Build
./gradlew build 명령어를 터미널에서 실행하여 Gradle 프로젝트를 빌드합니다.
이는 애플리케이션을 배포할 준비가 된 상태로 패키징하는 과정입니다.
프로젝트 내부에 포함된 Gradle Wrapper 파일(gradlew)를 통해 Gradle을 자동으로 다운로드하고, 지정된 버전의 Gradle로 빌드를 실행할 수 있습니다.
빌드 에러 나는 경우
1. 경로 확인 : 명령어를 실행하는 위치가 프로젝트 루트인지(gradle 파일이 존재하는곳)
2. 캐시 제거 : ./gradle clean 실행 후 다시 빌드
3. 테스트 모드 끄기 : ./gradlew build -x test
성공적으로 빌드하면 ./build/libs 경로에 빌드된 jar 파일이 생긴것을 확인 할 수 있습니다.
DockerFile
이제 빌드된 jar파일을 도커 컨테이너로 올리기 위해 도커 이미지로 만들어야 합니다. 도커 이미지는 컨테이너를 실행하는 데 필요한 모든 파일과 설정을 포함하는 읽기 전용 템플릿입니다.
도커 파일은 도커 이미지를 빌드하는 데 필요한 명령어들을 담은 텍스트 파일 입니다. 도커파일에 작성된 내용을 바탕으로 Docker 이미지를 빌드할 수 있습니다.
# Dockerfile
# 1. Base image 선택
FROM openjdk:17-jdk
# 2. 작업 디렉토리 설정
ARG JAR_FILE=build/libs/soltravel-0.0.1-SNAPSHOT.jar
# 3. 애플리케이션 jar 파일 복사
ADD ${JAR_FILE} soltravel-springboot.jar
# 4. 애플리케이션 실행 명령어
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/soltravel-springboot.jar"]
위와같이 도커 파일을 작성하고 프로젝트 루트에 넣어줍니다. 파일명은 반드시 Dockerfile 이여야 합니다.(f 소문자임)
Base image : 도커 이미지를 생성할 때 사용할 베이스 이미지를 정의합니다. 여기서는 OpenJDK 17의 JDK 이미지를 사용합니다.
ARG JAR_FILE: ARG 명령어는 빌드 타임에 사용하기 위한 변수를 할당하는 명령어입니다. JAR_FILE 변수에 앞서 생성한 jar파일의 경로를 할당합니다.
ADD : ADD 명령어는 로컬 파일 시스템에서 도커 이미지 내부로 파일을 복사합니다. # 2에서 ${JAR_FILE} 변수로 지정된 JAR 파일을 컨테이너의 루트 디렉토리에 soltravel-springboot.jar라는 이름으로 복사합니다.
ENTRYPOINT : ENTRYPOINT 명령어는 컨테이너가 시작될 때 실행될 기본 명령을 정의합니다.
- "java": Java 실행기를 호출합니다.
- "-Djava.security.egd=file:/dev/./urandom": Java 애플리케이션이 빠르게 랜덤 값을 생성할 수 있도록 설정하는 JVM 옵션입니다. 이는 특정 상황에서 애플리케이션의 시작 시간을 줄이는 데 도움을 줍니다.
- "-jar": JAR 파일을 실행하도록 Java에 지시합니다.
- "/soltravel-springboot.jar": 실행할 JAR 파일의 경로입니다. 앞서 복사한 JAR 파일이 이 경로에 존재하므로, 해당 JAR 파일이 실행됩니다.
Docker Image Build
이렇게 Dockerfile 설정을 마쳤다면 이제 도커 이미지를 빌드합니다.
마찬가지로 프로젝트 루트에서 docker build 명령어를 실행합니다.
docker build -t image-name:tag .
# docker build -t {도커허브 ID}/{도커허브 레포} .
생성할 이미지 이름과 태그(이미지 버전)을 명시해주고 ' . ' 으로 현재 디렉토리를 빌드 컨텍스트로 지정하여 현재 디렉토리에서 Dockerfile을 찾도록 명시해줍니다.
성공적으로 빌드가 수행되면 docker images 명령어로 방금 빌드한 이미지를 목록에서 확인할 수 있습니다.
Deploy EC2
로컬에서 컨테이너를 실행시킬 수도 있지만 저희는 EC2에 도커 컨테이너를 배포할것이기 떄문에 빌드한 도커 이미지를 EC2로 넘겨줘야합니다. SCP 명령어로 전송하는 방식과 도커 허브를 사용하는 방식 두가지가 존재하는데 이번 예제에서는 도커 허브로 진행하려고 합니다.
SCP는 간단하게 명령어로 전송할 수 있다는 장점이 있고 도커허브는 버전관리의 용이성과 일관성을 보장하기에 선택적으로 사용하면 될 것 같습니다.
도커 허브 회원가입 및 레포지토리 생성이 되었다고 가정하고 진행합니다. (찾아서 하면 금방해요)
DockerHub Push
docker tag 명령어로 로컬에서 빌드한 이미지를 도커 허브 레포지토리에 맞게 태깅합니다.
(이미지 명과 레포지토리 명이 같다면 따로 태깅하지 않아도됨)
만약 로컬에서 빌드한 이미지가 travelus:1.0이고 도커 허브 유저명/레포지토리명이 qoridhc/travelus 라면
# 이미지 태깅
docker tag {이미지명} {도커허브 ID}/{도커허브 레포}:{태그}
ex) docker tag travelus:1.0 qoridhc/travelus:1.0
# 도커 허브 push
docker push {도커허브 ID}/{도커허브 레포}:{태그} -> 태그 생략시 latest
ex) docker push qoridhc/travelus:1.0
이후 도커 허브를 확인해보면 성공적으로 명시된 레포지토리에 등록된것을 확인할 수 있습니다.
EC2
이제 도커 허브에 올라간 이미지를 ec2에서 pull 받기위해 ssh 등으로 ec2 터미널에 접근합니다.
도커 설치
Docker Docs를 참고해 EC2에 도커를 설치합니다, (https://docs.docker.com/engine/install/ubuntu/)
# 도커 apt 저장소 설치
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# GPG 키 등록
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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
hello-world 이미지가 정상적으로 출력이 된다면 성공적으로 도커 설치 완료한것입니다.
도커 허브 pull
docker pull {도커허브 ID}/{도커허브 레포}:your-tag
ex) docker pull qoridhc/travelus:1.0
docker pull 명령어로 도커 허브에 올려둔 레포지토리에서 이미지를 풀 받습니다.
docker images 명령어로 정상적으로 이미지가 풀받아 졌는지 확인합니다.
이미지 실행
docker run -d --name travelus-container -p 8080:8080 qoridhc/travelus:1.0
Docker run : 이미지 기반으로 컨테이너를 생성합니다. (-d 는 백그라운드로 실행하는것)
--name : 컨테이너 이름 명시
-p 호스트포트:컨테이너포트 : 호스트(EC2 인스턴스)의 8080 포트를 컨테이너의 8080 포트에 매핑. EC2 인스턴스의 8080 포트에 접근할 때 컨테이너의 8080 포트에 연결됩니다.
docker ps 명령어로 확인해보면 정상적으로 컨테이너가 구동된 것을 확인할 수 있습니다.
docker stop {컨테이너명 or ID}로 컨테이너 중단 가능
주의점
이미 한 번 이미지를 컨테이너화 했을 때, docker run 명령어를 다시 사용하면 안 됨
docker run 의 경우 Docker Hub에서 해당 이미지를 pull & 컨테이너화하는 작업이기 때문에, 중복 docker run 이후 docker ps -a 를 사용해보면 같은 이미지가 여러 개 생성되어 있는것을 확인 할 수 있음
따라서 이미 컨테이너가 존재하면 docker start {컨테이너명 or ID} 로 실행
마무리
프로젝트를 직접 빌드하고 도커 이미지화하여 도커 허브를 통해 ec2에 전송하고 해당 이미지를 컨테이너로 배포하는 과정을 수동으로 진행해보았습니다.
이렇게 수동으로 배포해도 어플리케이션 구동에는 문제가 없지만 개발과정에서 코드에 수정이 일어날때마다 이렇게 수동으로 배포하는것은 매우 번거로운 일입니다. 따라서 다음에는 이 모든 과정을 Jenkins라는 자동화 툴을 사용해서 새로운 코드가 깃에 push되면 알아서 빌드 & 배포를 진행하게끔 파이프라인을 구축해보겠습니다.