이전에는 GitHub Actions와 Elastic Beanstalk을 이용한 CI/CD에 대해 구축해보았지만 이번 프로젝트에는 다른 방법으로 도전해보았습니다. 저는 Jenkins와 Docker를 활용해서 CI/CD를 구축하는 방법에 대해 학습했는데, 그 과정과 결과를 기록하고자 합니다.
프리티어인 t2.micro EC2를 생성
default 용량인 8기가는 부족했던 경험이 있으므로 프리티어 최대 스토리지인 30기가로 설정!
EC2 에 적용할 보안그룹을 만들어 인바운드 규칙에 8080 포트 및 22(ssh) 포트를 열어주어야 한다.
도커를 설치하기 전 도커는 최소 4기가의 램이 필요하기 때문에 메모리 스와핑을 먼저 진행해야 한다.
메모리 스와핑 : 실제 Memory(Ram)가 가득 찼지만 더 많은 Memory 가 필요할때 디스크 공간을 이용하여 부족한 메모리를 대체할 수 있는 공간을 만드는 것
https://repost.aws/ko/knowledge-center/ec2-memory-swap-file
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon -s
sudo vi /etc/fstab
fstab 파일 끝에 다음 줄을 새로 추가하고 파일을 저장한 다음 종료
/swapfile swap swap defaults 0 0
https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
Install using the apt repository를 참고하였다.
# Add Docker's official GPG key:
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
# Add the repository to Apt sources:
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
sudo usermod -aG docker $USER
docker 를 sudo 명령어 없이 사용하기 위해 권한을 부여해 준다.
Jenkins 이미지 실행
docker run -d --name jenkins -p 8080:8080 -v /jenkins:/var/jenkins_home -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock -u root jenkins/jenkins:lts
주소창에 {ec2 퍼블릭 ip 주소}:8080 url 입력하면 해당 화면이 나오게 된다.
docker exec -it jenkins bash
cat /var/jenkins_home/secrets/initialAdminPassword << 네모박스 경로
→ 값 복사하여 password 에 집어넣기
Install Suggested plugin 클릭 및 회원가입을 완료한다.
github 메인 페이지 -> Settings -> Developer Settings -> Personal Access Tokens에 접근하여 토큰 발급
배포할 프로젝트 -> Settings -> Webhooks로 이동 -> Payload URL에는 {Jenkins 서버 주소}/github-webhook/을 입력
Jenkins 메인 화면 -> Jenkins 관리 -> Credentials
Username -> github 아이디
Password -> access token(Github Token)
ID -> 식별자 (jenkinsfile 에 넣어야 함)
pipeline 생성
Docker Hub -> My Account로 이동 → Security로 이동 → New Access Token을 통하여 토큰을 생성
토큰이 생성 후에, 이를 복사하여 Jenkins에 등록해줍니다.
젠킨스 환경변수 설정
환경변수(system) → Environment variables
Docker User Name(docker hub Id) , Docker Password(docker hub 엑세스 토큰 값) 설정
FROM openjdk:17-oracle
VOLUME /tmp
ARG JAR_FILE=/build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
pipeline {
agent any
stage('Clone Repository') {
steps {
git branch: 'main',
url: "https://github.com/alsduq1117/gameplanner.git",
credentialsId: 'minyeob' << 식별자 값
}
}
stage('Build Project') {
steps {
sh 'chmod +x gradlew'
sh './gradlew clean build'
}
}
stage('Docker Login') {
steps {
sh 'echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin'
}
}
stage('Build Docker Image') {
steps {
sh 'docker build -t alsduq1117/jenkins:test .'
}
}
stage('Push Docker Image') {
steps {
sh 'docker push alsduq1117/jenkins:test'
}
}
}
}
트러블 슈팅
Dockerfile에서 ARG JAR_FILE=/build/libs/.jar 이렇게 설정했는데, /build/libs/ 디렉토리에 jar 파일이 2개 이상(plain jar, jar)이라 와일드카드()를 사용하여 특정 jar 파일을 선택할 수 없는 문제
build.gradle
jar {
enabled = false
}
해당 코드를 추가해 주어 plain jar 가 생성되지 않도록 하였다.
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon -s
sudo vi /etc/fstab
fstab 파일 끝에 다음 줄을 새로 추가하고 파일을 저장한 다음 종료
/swapfile swap swap defaults 0 0
# Add Docker's official GPG key:
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
# Add the repository to Apt sources:
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
Jenkins 관리 -> Plugins 이동 -> Public Over SSH
, Post build task
플러그인 설치
System에 들어가서 Public over SSH 를 설정해 준다.
SSH 서버를 만들어 줍니다.
HostName : Application Sever IP
Username : ubuntu
test configure 을 눌렀을때 Success 가 나오면 Jenkins 서버에서 Application Server로의 연결이 완료된 것이다!
프로젝트에 scripts/DockerDeploy.sh 파일을 만들어준다.
#!/bin/bash
DOCKER_REPOSITORY=alsduq1117/jenkins
DOCKER_TAG=test
echo "> 현재 실행 중인 Docker Container ID 확인"
DOCKER_ID=$(docker ps -aqf "name=Jenkins")
echo "> 현재 실행 중인 Docker Container ID == $DOCKER_ID"
if [ -z "$DOCKER_ID" ]; then
echo "> 현재 실행 중인 Docker Container가 존재하지 않습니다."
else
echo "> 현재 실행 중인 컨테이너를 종료하겠습니다."
docker stop $DOCKER_ID
docker rm $DOCKER_ID
sleep 10
fi
echo "> 실행 중인 Docker Container를 종료하였습니다."
echo "> Docker Image를 삭제하겠습니다."
docker rmi $DOCKER_REPOSITORY:$DOCKER_TAG
echo "> Docker Image를 가져옵니다."
docker pull $DOCKER_REPOSITORY:$DOCKER_TAG
echo "> Docker Image를 실행합니다."
docker run -d -p 8080:8080 --name Jenkins $DOCKER_REPOSITORY:$DOCKER_TAG
ssh 통신으로 jar 파일과 쉘 스크립트 파일을 전달 후, 이를 실행하여 Application Server에서 Docker 이미지를 띄우도록 하였다.
stage('Push Docker Image') {
steps {
sh 'docker push alsduq1117/jenkins:test'
}
}
stage('Deploy') {
steps {
sshPublisher(
continueOnError: false, failOnError: true,
publishers: [
sshPublisherDesc(
configName: 'ApplicationServer',
verbose: true,
transfers: [
sshTransfer(
sourceFiles: '$JAR_FILE_PATH,$SCRIPT_FILE_PATH',
removePrefix: '',
remoteDirectory: '/',
execCommand: '''
echo "JAR_FILE_PATH: ${JAR_FILE_PATH}"
echo "SCRIPT_FILE_PATH: ${SCRIPT_FILE_PATH}"
echo "Starting deployment..."
sudo chmod 777 /home/ubuntu/app/$SCRIPT_FILE_PATH
sudo chmod 777 /home/ubuntu/app/$JAR_FILE_PATH
echo "Running DockerDeploy.sh..."
/home/ubuntu/app/$SCRIPT_FILE_PATH
echo "Deployment finished."
'''
)
]
)
]
X )
}
}
...
System에 들어가 Jenkins 환경변수에 추가해 줍니다.
JAR_FILE_PATH -> jar 파일이 존재하는 위치 ex) build/libs/myproject-0.0.1-SNAPSHOT.jar
APPLICATION_SERVER -> Application Server의 IP주소
SCRIPT_FILE_PATH -> 스크립트 파일이 존재하는 위치 ex ) scripts/DockerDeploy.sh
MySql RDS 생성
application.yml 파일
spring:
datasource:
url: jdbc:mysql://${RDS_HOSTNAME}:${RDS_PORT}/${RDS_DB_NAME}
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${RDS_USERNAME}
password: ${RDS_PASSWORD}
DB를 연결하여 Database를 하나 생성해줍니다 >> RDS_DB_NAME
System에 들어가 Jenkins 환경변수에 추가해 준다.
RDS_HOSTNAME : RDS 엔드포인트 값
RDS_PORT : 3306
RDS_DB_NAME : 생성해준 Database 이름
RDS_USERNAME : 마스터 사용자 이름
RDS_PASSWORD : 마스터 암호
추가적으로 Application Server
에 연결하여 /home/ubuntu/app 경로에 env 파일을 하나 생성해 준다.
vim .env << 명령어 실행
RDS_HOSTNAME : RDS 엔드포인트 값
RDS_PORT : 3306
RDS_DB_NAME : myDatabase 생성해준 Database 이름
RDS_USERNAME : admin 마스터 사용자 이름
RDS_PASSWORD : 마스터 암호
해당하는 값들을 집어넣어 준다.
Docker가 환경 변수를 참조하여 명령어를 실행할 수 있도록 다음처럼 쉘 스크립트를 다음과 같이 수정해준다.
... ...
echo "> Docker Image를 실행합니다."
docker run -d -p 8080:8080 --name Jenkins --env-file /home/ubuntu/app/.env $DOCKER_REPOSITORY:$DOCKER_TAG
다음과 같이 정상적으로 배포가 완료된다.
처음에는 Jenkins 컨테이너를 아래와 같이 간단하게 실행했습니다.
docker run -d --name Jenkins -p 8080:8080 jenkins/jenkins:lts-jdk17
docker run -d --name Jenkins -p 8080:8080 jenkins/jenkins:lts-jdk17
그러나 이렇게 설정하고 진행하다 보니, Jenkins 내부에서 Docker 이미지를 빌드하고 푸시하는 과정에서 Docker의 명령어를 사용해야 하는 상황이 발생했습니다. 이럴 경우 Jenkins 내부에 Docker를 추가로 설치해야 하는 문제가 생기는데, 이는 불필요한 리소스 낭비를 초래할 수 있습니다.
이를 해결하기 위해, Jenkins 이미지를 실행할 때 Docker socket을 사용하도록 설정했습니다. 아래의 명령어는 Jenkins 컨테이너를 실행하면서 Docker socket을 연결하는 방법을 통해 문제를 해결하였습니다
docker run -d --name jenkins -p 8080:8080 -v /jenkins:/var/jenkins_home -v /usr/bin/docker:/usr/bin/docker -v /var/run/docker.sock:/var/run/docker.sock -u root jenkins/jenkins:lts
jenkins가 private repository 에 접근하지 못하여 초기에 Clone Repository를 제대로 하지 못했습니다. 따라서 Credencials 을 만들때 다음 값들을 넣어주고
Username -> github 아이디
Password -> access token
ID -> 식별자를 지정
pipeline {
agent any
stages {
stage('Clone Repository') {
steps {
git branch: 'main',
url: "https://github.com/alsduq1117/gameplanner.git",
credentialsId: 'your-credentials-id'
}
}
}
}
credentialsId 를 위 코드와 같이 제공해줘서 읽어올 수 있도록 하였습니다.
이렇게 Jenkins와 Docker를 활용한 CI/CD 구축 방식을 적용해보니, 기존에 사용하던 GitHub Actions와 Elastic Beanstalk을 이용한 방식에 비해 빌드 속도가 크게 단축된 것을 경험할 수 있었습니다. 이는 프로젝트의 전반적인 개발 효율성을 높이는 데 크게 기여하였습니다.이 글을 통해 저와 같이 새로운 CI/CD 방식에 관심이 있는 분들에게 도움이 되었으면 좋겠습니다.