이번 Quizeloper 프로젝트를 진행하면서 CICD는 내가 담당하게 되었다!
구축하면서 진행한 과정 및 공부한 내용을 기록해보려고 한다.
yaml 보안파일 함께 올리고 ec2 용량 늘리기 등 ,, 고군분투 끝에 성공했다 허허
구현 방식
구성 요소
- Jenkins Server : AWS EC2 Ubuntu 20.04
- Spring Boot Server : AWS EC2 Ubuntu 20.04
- Github Repository
- Docker Hub Repository
- SpringBoot : 3.1.2
- Java : OpenJDK 17
진행 순서
- Jenkins Server에 Docker 설치
- Jenkins Server에 Docker를 이용하여 Jenkins 실행
- Jenkins 접속
- Jenkins와 Github 연동
- Jenkins와 Docker Hub 연결
- Jenkins Server와 Spring Boot Server SSH 연결 설정
- Jenkins Pipeline 구성
- Spring Boot Project Github Repository Clone
- application-{secret}.yaml 파일 추가
- Gradle Build
- Docker Build
- Docker Push
- Spring Boot Server SSH 연결
- Docker Pull
- Docker Run
✅EC2 Jenkins Server 구축
🪴EC2 초기 설정
sudo apt-get update && sudo apt-get upgrade
sudo apt install build-essential
🪴Docker 설치
1. 기본 설정 & 사전 설치
sudo apt update
sudo apt install apt-transport-https ca-certificates curl software-properties-common
2. 자동 설치 스크립트 활용
리눅스 배포판 종류를 자동으로 인식하여 Docker 패키지를 설치해주는 스크립트를 제공
sudo wget -q0- https://get.docker.com/ | sh
3. Docker 서비스 실행하기 및 부팅 시 자동 실행 설정
sudo systemctl start docker
sudo systemctl enable docker
4. Docker 그룹에 현재 계정 추가
sudo usermod -aG docker ${USER}
sudo systemctl restart docker
- docker 그룹은 root 권한과 동일하므로 꼭 필요한 계정만 포함
- 현재 실행 중인 계정에서 로그아웃한 뒤 다시 로그인하면 적용
- Docker 명령어 작성할 때 sudo 없이 작성할 수 있어서 매우 편리하므로 사용하자!
5. Docker 설치 확인
docker -v
위와 같이 도커 설치된 것을 확인할 수 있다!
🪴Docker로 Jenkins 설치하기
1. Jenkins 이미지 파일 내려받기 (JDK 17)
docker pull jenkins/jenkins:jdk17
보통 lts 버전으로 내려받는데, 나는 jdk17버전을 사용했다.
스프링부트 버전이랑 자바 버전이 높은 편이라 lts를 사용하는 경우 젠킨스 gradle로 빌드하는 과정에서 오류가 생겼다.
자바 버전이 높은 경우 (17 이상), 혹은 빌드 단계에서 버전 관련 에러가 생기는 경우 jenkins 버전을 직접 명시에 다운 받길 권한다!
다운받으면 이런식으로 출력이 된다.
2. 내려받아진 이미지 확인
docker images
3. Jenkins 이미지를 Container로 실행
docker run -d -p 8080:8081 -p 50000:50000 -v /jenkins:/var/jenkins -v /home/ubuntu/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock --name jenkins -u root jenkins/jenkins:jdk17
- docker run -d : Docker 컨테이너를 백그라운드에서 실행하는 명령어
- -p 8080:8081 : 호스트의 8080 포트와 컨테이너의 8081 포트를 연결하여 Jenkins 웹 인터페이스에 접근
- -p 50000:50000: 호스트의 50000 포트와 컨테이너의 50000 포트를 연결하여 Jenkins 에이전트 노드와의 통신을 위해 사용
- -v /jenkins:/var/jenkins: 호스트의 /jenkins 경로와 컨테이너의 /var/jenkins 경로를 볼륨으로 연결
Jenkins 설정 및 데이터를 호스트에 저장 - -v /home/ubuntu/.ssh:/root/.ssh: 호스트의 /home/ubuntu/.ssh 경로와 컨테이너의 /root/.ssh 경로를 볼륨으로 연결
SSH 키와 관련된 데이터를 호스트와 공유 - -v /var/run/docker.sock:/var/run/docker.sock: 호스트의 Docker 소켓과 컨테이너의 Docker 소켓을 연결하여 컨테이너 내부에서 호스트의 Docker 데몬 제어
- --name jenkins: 컨테이너의 이름을 "jenkins"로 지정
- jenkins/jenkins:jdk17: 실행할 Docker 이미지를 지정
나는 호스트랑 컨테이너 포트를 분리하기 위해 8080, 8081로 나누었고, OpenJDK17버전으로 이미지를 지정하였다!
4. 돌아가고 있는 Container 확인
docker ps
🪴EC2 프리티어 용량 확장
프리티어 기준 EC2를 사용해 Jenkins를 빌드하는 경우 중간에 멈추거나 팅긴다 ..
CPU 사용량만 봐도 널뛰기마냥 날뛰기에, swap 메모리를 할당해서 이러한 문제를 방지하려고 한다!
프리티어 EC2 기본 RAM이 1GB이니, 2GB(128MB * 16) swap 파일을 만들어 할당한다.
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
/swapfile swap swap defaults 0 0
이렇게 해도 용량이 부족한 경우, 이 블로그 글을 참고하길 바란다!
빌드가 계속 안되길래 용량이 많이 부족한 줄 알고 참고해서 인스턴스 볼륨 크기를 16GiB까지 늘렸으나, 지금 보니 파이프라인 도커 실행 단계에서 명령어를 잘못 친 것 때문인 듯하다. 허허 그래도 마음 편하게 늘리면 좋으니 ..
참고로 EBS 용량은 30GiB가 최대라고 한다.
✅Jenkins 설정
🪴Jenkins 접속
1. 브라우저에서 [EC2 인스턴스 URL]:8081로 접속
참고로 나는 도커 실행할 때 호스트 포트를 8081로 열어주었다!
2. 암호 입력
접속하면 다음과 같은 화면이 뜬다.
초기 젠킨스 비밀번호를 확인해야하는데 Jenkins Container에 접속하해서 얻어오거나 docker logs jenkins 명령어를 사용하면 된다.
// 컨테이너에 접속하지 않고 확인
docker logs jenkins
// Container 접속해서 암호 파일 확인
docker ezezc -it jenkins bash
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
3. Install suggested Plugins로 플러그인 설치
웬만하면 다 성공하지만 간혹가다 다 설치 실패로 뜨는 경우가 있는데, 이런 경우 젠킨스 서버와의 버전이 맞지 않아 생기는 오류일 가능성이 크다. 서버 버전과 맞추고 난 후 다시 다운받길 바란다!
4. Jenkins 계정 생성
계정명, 암호는 젠킨스 서버에 접속할 때 필요한 Id, Password이다.
로그인한 뒤 상단과 같이 Jenkins 메인 대시보드 페이지가 나오면 성공이다!
🪴Jenkins Plugin 설치
Docker, Jenkins로 스프링부트 프로젝트를 빌드하기 위해 ssh 관련 데이터 및 docker 정보를 추가해야한다.
또한 플러그인을 설치해서 연결하는 과정이 필요하다.
우선, 사용에 필요한 플러그인 리스트를 보고 다운받자.
1. Plugin 설치
Jenkins 대시보드 > Jenkins 관리 > Plugins > Available Plugins 접속
2. 설치 Plugin 리스트
Docker, Docker Pipeline, SSH Agent, Publish Over SSH, Github Integration을 차례로 다운받는다.
다운 받고 바로 시작할 수 있으면 그대로 진행하고, 재부팅이 필요할시 젠킨스 컨테이너 중지하고 재시작을 한다.
🪴Jenkins & Github 연동
Jenkins Container를 생성할 때 "/home/ubuntu/.ssh:/root/.ssh"로 .ssh 디텍도리를 마운트 해놓았기 때문에 Container 밖에서 ssh 키를 생성하면 Jenkins Container와 연결된다.
1. SSH 키 생성
ssh-keygen
그냥 전부 enter를 입력해 default로 만든다.
다음과 같은 Key print 이미지가 생성되면 성공이다.
2. Github Deploy Key 등록
연동할 Github Repository의 Settings > Deploy Keys로 접속한다.
제목은 자유롭게 적어주고 Key 부분에 id_rsa.pub에 들어있는 public key 값을 넣어준다.
하단의 명령어로 id_rsa.pub을 확인할 수 있다.
cd /home/ubuntu/.ssh
cat id_rsa.pub
3. Jenkins Credentials 등록
Jenkins 대시보드 > Jenkins 관리 > Credentials에 접속한다.
Stores scoped to Jenkins의 Domains가 (global)이라 적혀있는 부분을 누른다.
Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동한다.
왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가한다.
- Kind
SSH Username with private Key - ID
github → 마음대로 지어도 된다. 그러나 Pipeline Script 작성시 기입해야하기에 식별할 수 있는 ID로 작성하자. - Username
root (Default) - Private Key
Enter directly 체크 → private key 입력
private key는 Jenkins Server에서 생성된 id_rsa로, 아래 명령어로 확인할 수 있다.
cd /home/ubuntu/.ssh
cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
...
...
...
-----END OPENSSH PRIVATE KEY-----
🪴Jenkins & Docker Hub 연동
1. Docker Hub Credentials 등록
첫 부분에서 Docker 관련 Plugin을 설치했다.
Github ssh key 등록한 것과 마찬가지로 Docker Hub Credentials를 등록한다.
Jenkins 대시보드 > Jenkins 관리 > Credentials에 접속한 뒤 Add Credentials를 눌러 credentials를 추가한다.
- Kind
Username with password - Username
본인의 Docker Hub ID - Password
본인의 Docker Hub Password - ID
docker-hub → 마음대로 지어도 된다. 그러나 Pipeline Script 작성시 기입해야하기에 식별할 수 있는 ID로 작성하자.
2. Jenkins Container 내부에 Docker 설치
Jenkins Pipline에서 Docker 명령어를 사용할 수 있도록 Jenkins Container 내부에 Docker를 설치해야 한다.
EC2에서 Jenkins Container에 접속한다.
docker exec -it jenkins /bin/bash
apt-get update
apt-get install docker.io
🪴Jenkins & SSH 연동
지금까지는 Jenkins Server에서 jenkins 컨테이너에 접속하지 않고 key들을 접근하여 Jenkins Server의 key 경로를 모른다. 즉 /home/ubuntu/.ssh/id_rsa를 입력하면 안된다.
그러나 Jenkins Server에서 컨테이너를 실행할 때 /home/ubuntu/.ssh를 /root/.ssh와 연결해 놓았기 때문에 Jenkins 서버 내 /root/.ssh/id_rsa를 입력하면 된다.
이를 위해 Jenkins 상에서 Publish Over SSH 플러그인을 미리 설치해둔 상태이다.
Publish Over SSH 플러그인 설정
Jenkins 대시보드 > Jenkins 관리 > 시스템 설정에서 Publish Over SSH 영역의 고급 버튼을 눌러 설정
- Path to Key
Private key의 경로를 입력한다. → /root/.ssh/id_rsa - Key
id_rsa 파일 내용 복사해서 붙여넣기 - Name
접속할 ssh 서버의 이름 입력 - Hostname
접속할 서버의 주소 입력
Spring Boot Server EC2 인스턴스 URL을 넣어준다. - Username
접속할 유저명으로, 기본 유저인 ubuntu를 입력한다 - Test Configuration 버튼을 눌러 정상적으로 연결 되는지 확인한다!
🪴Application-{secret}.yaml 추가
보통 스프링부트 프로젝트를 작업할 때 보안 관련 파일 (application-{secret}.yaml)을 ignore로 처리하는 경우가 많을 것이다.
이런 경우 젠킨스에서 빌드할 때 직접 파일을 추가해주지 않으면 인식하지 못해 build에 실패한다.
application-secret.yaml 파일을 build할 때 넣어줄 수 있도록 pipeline script에 쓰일 식별자를 만들 것이다.
Jenkins 관리 > Credentials > System > Global Credentials에 add Credentials 버튼을 누른다.
- Kind
Secret file - File
application-{secret}.yaml에 해당하는 파일을 올려준다. - ID
pipeline script에서 식별될 식별자
생성해주면 끝!
참고로 나는 Build할 때 Test 관련 DB를 따로 설정해주지 않아 Test 단계에서 Build가 실패했었다.
아직 프로젝트 개발 시작 전이고 Test 작업을 시작하지 않아서 pipeline script에 추가로 (프로젝트명)ApplicationTests.java 파일을 지우고, @SpringBootTest 어노테이션을 지운 상태의 (프로젝트명)ApplicationTests.java 파일이 copy되는 명령어를 추가로 기입하였다.
어노테이션을 지운 상태의 파일을 넣기 위해 application-{secret}.yaml 파일을 추가로 넣어준 것처럼 Credentials를 만들었다.
이와 관련하여 트러블슈팅 글을 작성할 예정이다.
✅Jenkins Pipeline
이제 본격적으로 pipeline을 구성할 차례이다.
Jenkins 대시보드 > 새로운 item을 누르고 적절한 item 이름을 입력한 뒤 Pipeline을 선택한다.
🪴Jenkins & Github Webhook 연동
깃허브에 푸시할 때마다 자동으로 Build 될 수 있도록 Pipeline과 Github Webhook을 연동해야 한다.
초반에 이에 필요한 Plugin인 Github Integration Plugin을 설치한 상태이다.
1. Github Webhook 추가
Github Repository에서 Settings > Webhooks > Add Webhook을 눌러 Webhook을 추가한다.
- Payload Url
[Jenkins Server URL]:[Jenkins Server 포트]/github-webhook/ - Content type
application/json
나머지는 모두 default 설정을 유지하고, Add webhook 버튼을 눌러 Wehhook을 추가한다.
github-webhook 뒤의 /를 꼭 넣어줘야 한다!
이때 꼭 보안그룹의 인바운드 규칙에 젠킨스 포트를 0.0.0.0으로 퍼블릭으로 열어두어야 깃허브가 보낸 Webhook 요청을 받을 수 있다.
2. Jenkins Pipeline 설정
아까 생성한 Pipeline 창에 다시 가서 General 칸에 Github Project를 선택한 후 자신의 Github Repository 링크를 입력한다.
이때 Repository Url은 Clone시 사용하는 HTTPS Url(.git으로 끝남)으로 입력한다.
빌드 유발에는 깃허브 webhook 설정을 위해 GitHub hook trigger for GITScm polling을 체크해준다.
Pipeline Script에 다음과 같이 작성한다.
pipeline {
agent any
environment {
imagename = "Docker 유저 아이디/이미지 이름"
registryCredential = 'Docker hub Credential Id'
dockerImage = ''
}
stages {
stage('Prepare') {
steps {
echo 'Clonning Repository'
git url: 'Github Repository SSH Url(git@github.com로 시작)',
branch: 'Clone 받아올 브랜치 이름',
credentialsId: 'Github Credential Id'
}
post {
success {
echo 'Successfully Cloned Repository'
}
failure {
error 'This pipeline stops here...'
}
}
}
// 보안 정보 다운로드
stage('secret.yml download') {
steps {
withCredentials([file(credentialsId: 'application-secret Credential Id', variable: 'dbConfigFile')]) {
script {
// Secret file 입력할 주소
sh 'cp $dbConfigFile /var/jenkins_home/workspace/(프로젝트 이름)/src/main/resources/application-secret.yaml'
}
}
}
}
// SpringBootTest 어노테이션 삭제
stage('spring test rm') {
steps {
withCredentials([file(credentialsId: 'spring test Credential Id', variable: 'testConfigFile')]) {
script {
sh 'rm /var/jenkins_home/workspace/(프로젝트명)/.../ApplicationTests.java' // 기존 파일 삭제
sh 'cp $testConfigFile /var/jenkins_home/workspace/(프로젝트명)/.../ApplicationTests.java' // 새로 파일 업로드
}
}
}
}
stage('Bulid Gradle') {
steps {
echo 'Bulid Gradle'
dir('.'){
sh './gradlew clean build --no-daemon' // 빠른 빌드를 위해 daemon 조건 없애기
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Bulid Docker') {
steps {
echo 'Bulid Docker'
script {
dockerImage = docker.build imagename
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Push Docker') {
steps {
echo 'Push Docker'
script {
docker.withRegistry( '', registryCredential) {
dockerImage.push()
}
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Docker Run') {
steps {
echo 'Pull Docker Image & Docker Image Run'
sshagent (credentials: ['ssh credential Id']) {
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker pull (위에서 설정했던 도커 이미지 이름)'"
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker ps -q --filter name=(도커 컨테이너 이름) | grep -q . && docker stop (도커 컨테이너 이름) && docker rm (도커 컨테이너 이름) | true'"
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker run -d --name quiz -p 8080:8081 --name=(도커 컨테이너 이름) (도커 이미지 이름) -f /dev/null'" // 종료하지 않고 계속 실행
}
}
}
}
}
하단에 도커 이미지 이름은 (docker hub ID)/(프로젝트명)으로 구성하고, 컨테이너 이름은 (docker hub ID)-(프로젝트명)이다.
이미지 이름 형식 저렇게 안지어서 계속 오류 났었다 ..ㅎ
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker pull (위에서 설정했던 도커 이미지 이름)'"
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker ps -q --filter name=(도커 컨테이너 이름) | grep -q . && docker stop (도커 컨테이너 이름) && docker rm (도커 컨테이너 이름) | true'"
sh "ssh -o StrictHostKeyChecking=no [스프링부트 Server username]@[스프링부트 Server IP 주소] 'docker run -d --name quiz -p 8080:8081 --name=(도커 컨테이너 이름) (도커 이미지 이름) -f /dev/null'" // 종료하지 않고 계속 실행
첫번째 line은 올렸던 도커 이미지를 받아오는 명령어이다.
두번째 line은 현재 실행중인 도커 컨테이너를 찾고 종료하는 명령어다.
처음 Build할 때는 두번째 line을 주석 처리하고, 두번째 Build부터 사용하면 에러가 나지 않는다.
마지막 line은 도커 컨테이너의 8081 포트를 EC2 인스턴스의 8080 포트와 연결하여 접근할 수 있도록 하는 명령어이다.
이렇게 해서 빌드하면!
docker push 에러가 발생한다.
젠킨스 서버의 도커 컨테이너에 로그인을 안한 상태로, 다음과 같은 명령어로 로그인하면 된다.
docker login
// user : 도커 허브 아이디
// password : 도커 허브 비밀번호
13번 (사실 그 이상) 끝에 성공!
중간에 용량 및 버전 이슈가 있었지만 ..
묵히고 묵히던 CI/CD 진행 과정을 정리할 수 있어서 뿌듯하다 ㅎㅎ
앞으로의 프로젝트 개발도 잘 할 수 있길 바라며 이상 포스팅을 마친다!
➕References
[Devops] Jenkins, Docker로 Spring Boot CI/CD 구축하기 (tistory.com)
'DevOps' 카테고리의 다른 글
[CI/CD] CI/CD란 (0) | 2023.01.31 |
---|