지난 포스팅 에 이어 MSA 적용을 하면서, CI/CD 파이프라인을 구축하려 한다.
기존에는 Github Actions을 사용했지만, 프로젝트 규모가 커지다 보면 속도 측면에 Jenkins보다 느리기 때문에 Jenkins로 변경하기로 했습니다.
- Jenkins Server : AWS EC2 Ubuntu 18.04
- Spring Boot Server : AWS EC2 Ubuntu 18.04
- Github Repository
- Docker Hub Repository
인스턴스 2개를 팔 예정이다(Jenkins용 , Spring 서버용)
인스턴스 생성하는 과정은 생략할 예정입니다!!
- Jenkins Server에 Docker 설치
- Jenkins Server에 Docker를 이용하여 Jenkins 실행
- Jenkins 접속
- Jenkins와 Github 연동
- Jenkins와 Docker Hub 연결
- Jenkins Server와 Spring Boot Server SSH 연결 설정
- Jenkins와 Slack 연동
- Jenkins Pipeline 구성
- Spring Boot Project Github Repository Clone
- application.yml Jenkins로 관리
- Gradle Build
- Docker Build
- Docker Push
- Spring Boot Server SSH 연결
- Docker Pull
- Docker Run
- Slack Notification
사전에 Jenkins Server, Spring Boot Server를 위한 AWS EC2
인스턴스 2개와 Spring Boot Project가 올라가있는 Github Repository와
Docker Hub Repository가 생성되어 있어야 한다.
대부분의 설정은 Jenkins Server에 접속하여 진행한다.
Spring Boot Server는 SSH 연결 설정 시 public key를 등록할 때만 접속한다.
sudo apt update
sudo apt upgrade
sudo apt install build-essential
1. 기본 설정, 사전 설치
sudo apt update
sudo apt install apt-transport-https ca-certificates curl software-properties-common
2. 자동 설치 스크립트 활용
sudo wget -qO- 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
1. Jenkins 이미지 파일 내려받기(lts 버전)
docker pull jenkins/jenkins:lts
2. 내려받아진 이미지 확인
docker images
3. Jenkins 이미지를 Container로 실행
docker run -d -p 8080:8080 -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:lts
4. 돌아가고 있는 Container 확인
docker ps
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
// Container 접속
$ docker exec -it jenkins bash
// 암호 파일 확인
$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword
1. ssh키 생성
Jenkins Container를 생성할 때 "/home/ubuntu/.ssh:/root/.ssh"로 .ssh 디텍도리를 마운트 해놓았기 때문에 Container 밖에서 ssh 키를 생성하면 Jenkins Container와 연결된다.
ssh-keygen
// 아래 사진과 같이 나머지 값들 그냥 전부 enter를 입력해 default로 만든다.
2. Github Deploy Key 등록
만들어 놓은 Github Repository > Settings > Deploy Keys > Add deploy key
로 접속한다.
Title은 Jenkins로 지어 주고(임의의 이름), Key 부분에 id_rsa.pub에 들어있는 public key 값을 넣어준다. 아래 명령어로 확인 가능하다.
cd /home/ubuntu/.ssh
cat id_rsa.pub
3. Jenkins Credentials 등록
Jenkins 대시보드 > Jenkins 관리 > Manage Credentials > Credentials에 접속한다.
Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동한다.
왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가한다.
다만 Pipeline Script 작성 시 credentialsId로 사용되니 식별할 수 있도록 하자
Jenkins Server
에서 생성한 id_rsa
이다. 아래 명령어로 확인 가능하다.cd /home/ubuntu/.ssh
cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
...
... 이와 같은 형태의 key가 private key 입니다 ...
...
-----END OPENSSH PRIVATE KEY-----
-----BEGIN OPENSSH PRIVATE KEY-----, -----END OPENSSH PRIVATE KEY----- 이부분도 포함해야한다!
1. Docker Plugin 설치
Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > Docker 검색 > Docker, Docker Pipeline 플러그인 설치 및 재실행
2. Docker Hub Credentials 등록
Jenkins 대시보드 > Jenkins 관리 > Manage Credentials > Credentials
에 접속한다. (상단 Jenkins Credentials 등록과 같은 방식)
Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동한다.
왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가한다.
OK를 눌러 키를 생성
3. Jenkins Container 내부에 Docker 설치
Jenkins Pipeline에서 Docker 명령어를 사용할 수 있도록 Jenkins Container 내부에 Docker를 설치해야 한다.
docker exec -it jenkins bash
docker run -d -p 8080:8080 -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:lts
Jenkins Container를 가동할 때 위에 처럼 외부 Docker volume을 연결해놓아서 아래처럼 docker.sock 권한 변경만 하면 Container 내부에서 docker 명령어가 사용 가능하다고 하는데 Pipeline Script를 작성하고 Build했더니 docker 명령어 사용 부분에서 command not found 에러가 발생하여 그냥 Container 내부에도 Docker를 설치하였다.
Docker 설치 방법은 위에서 EC2 내에 Docker를 설치한 방법과 동일하다. 다만 Container 내부에 sudo, vi, wget이 설치되어 있지 않아 차례대로 설치 후 Docker를 설치해야 한다.
sudo chmod 666 /var/run/docker.sock
Docker가 설치되어 있는 Jenkins 이미지 파일을 사용한다.
docker exec 명령어로 Jenkins Container 내부로 접속한 다음 내부에 Docker를 설치한다.
docker 경로와 docker.sock 파일 경로를 도커 볼륨에 추가해 사용한다.
이 방법을 사용하더라도 Container 내부에 docker-cil는 설치해야 한다는 글을 보았다.
실험 필요.
Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > SSH Agent 플러그인을 검색하고 설치 및 재실행
// Jenkins Server에서 public key 확인
$ cat /home/ubuntu/.ssh/id_rsa.pub
// Spring Boot Server에 Jenkins Server public key 추가
$ vi /home/ubuntu/.ssh/authorized_keys
🔎
id_rsa.pub
출력하면 맨 뒤에 유저네임과 ip주소가 나올수도 있다. 상관없이 다 포함해서 Spring Boot Instance에 추가해준다!! 아래 사진과 같이 그리고 Spring Boot Instanceauthorized_keys
파일에 pem키에 대한 정보가 있을텐데, 그 정보 마지막 줄에서 한 칸 뛰고id_rsa.pub
키를 추가해주면 된다.
Jenkins 대시보드 > Jenkins 관리 > Manage Credentials > Credentials에 접속한다.
Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동한다.
왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가한다.
id_rsa
이다. 아래 명령어로 확인 가능하다.cd /home/ubuntu/.ssh
cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
...
... 이와 같은 형태의 key가 private key 입니다 ...
...
-----END OPENSSH PRIVATE KEY-----
Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > Publish Over SSH 플러그인을 검색하고 설치 및 재실행
1. Publish Over SSH 플러그인 설정
Jenkins 대시보드 > Jenkins 관리 > System에서 Publish Over SSH 영역의 고급 버튼을 눌러 설정
Slack Notification Plugin 설치
Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > Slack Notification 플러그인을 검색하고 설치 및 재실행
Slack에 Jenkins 앱 추가
Slack 앱에서 Slack 찾아보기 > 앱 >jenkins 검색 > jenkins
선택 후 연동을 하면 연동 가이드 웹페이지에 하위 도메인과 토큰이 출력된다. 이를 이용하여 Slack Credentials을 등록한다.
3. Slack Credentials 등록
이제 드디어 Pipeline을 작성하면 된다. Jenkins 대시보드 > 새로운 Item에서 item name을 입력하고 Pipeline을 선택, OK 버튼을 누른다.
Github Repository에 push event가 발생했을 때 자동으로 Build가 실행되게 하기 위해 Pipeline과 Github Webhook
을 연동해야 한다.
1. Github Integration Plugin 설치
Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > Github Integration 플러그인
을 검색하고 설치 및 재실행
2. Jenkins Pipeline 설정
Pipeline 구성(톱니바퀴 버튼) > General
영역에서 Github project를 선택한다. Project url에 본인의 Github Repository Url을 입력한다. 이 때 Repository Url은 Clone 시 사용하는 HTTPS Url(.git으로 끝남)을 입력한다.Pipeline 구성(톱니바퀴 버튼) > Build Triggers
영역에서 GitHub hook trigger for GITScm polling
을 선택한다.3. Github Webhook 추가
Github Repository에서 Settings > Webhooks > Add Webhook
을 눌러 Webhook을 추가한다.
pipeline {
agent any
environment {
imagename = "docker build로 만들 이미지 이름"
registryCredential = '위에서 만든 Docker Hub Credential ID'
dockerImage = ''
}
stages {
stage('Clonning Repository') {
steps {
echo 'Clonning Repository'
git url: 'Github Repository SSH Url(git@github.com로 시작)',
branch: 'Clone 받아올 Branch 이름',
credentialsId: '위에서 만든 Github Credential ID -> github'
}
post {
success {
echo 'Successfully Cloned Repository'
}
failure {
error 'This pipeline stops here...'
}
}
}
stage('Bulid Gradle') {
steps {
echo 'Bulid Gradle'
dir('.'){
sh './gradlew build '
}
}
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 -> ssh']) {
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker rm -f [컨테이너 이름] || true'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker ps -q --filter name=[컨테이너 이름] | grep -q . && docker rm -f \$(docker ps -aq --filter name=[컨테이너 이름])'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker run -d --name [컨테이너 이름] -p 8080:8080 [도커이미지 이름]'"
}
}
}
}
post {
success {
slackSend (channel: '#채널명', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
failure {
slackSend (channel: '#채널명', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
pipeline {
agent any
environment {
imagename = "docker build로 만들 이미지 이름"
registryCredential = 'Docker Hub Credential ID'
dockerImage = ''
sshCredential = 'SSH Credential ID'
}
stages {
stage('Prepare') {
steps {
echo 'Cloning Repository'
script {
deleteDir()
git branch: 'branch 이름',
credentialsId: 'Github Credential ID ',
url: 'Github Repository SSH Url(git@github.com로 시작)',
changelog: true,
poll: false,
script: {
dir('branch/가져올 프로젝트 폴더 이름') {
// Additional steps specific to the folder, if needed
echo 'Cloning specific folder: branch/가져올 프로젝트 폴더 이름'
}
}
}
}
post {
success {
echo 'Successfully Cloned Repository'
}
failure {
error 'This pipeline stops here...'
}
}
}
stage('Bulid Gradle') {
steps {
echo 'Bulid Gradle'
dir('가져올 프로젝트 폴더 이름/'){
sh './gradlew build'
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Bulid Docker') {
steps {
echo 'Bulid Docker'
dir('가져올 프로젝트 폴더 이름/'){
script {
dockerImage = docker.build imagename
}
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Push Docker') {
steps {
echo 'Push Docker'
dir('가져올 프로젝트 폴더 이름/'){
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: [sshCredential]) {
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker rm -f [컨테이너 이름] || true'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker ps -q --filter name=[컨테이너 이름] | grep -q . && docker rm -f \$(docker ps -aq --filter name=[컨테이너 이름])'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker run -d --name [컨테이너 이름] -p 8080:8080 [도커이미지 이름]'"
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
}
post {
success {
slackSend (channel: '#채널명', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
failure {
slackSend (channel: '#채널명', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
보통 프로젝트를 github에 올리게 된다면, 비밀정보가 들어있는 yml,properties 파일을 .gitignore 시켜서 올립니다. 그런 상황에서 jenkins를 통해 관리하고 파이프라인을 알아보도록 하겠습니다!
1. application.yml Credentials 등록
Jenkins 대시보드 > Jenkins 관리 > Manage Credentials > Credentials에 접속한다.
Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동한다. 왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가한다.
pipeline {
agent any
environment {
imagename = "docker build로 만들 이미지 이름"
registryCredential = 'Docker Hub Credential ID'
dockerImage = ''
sshCredential = 'SSH Credential ID'
}
stages {
stage('Prepare') {
steps {
echo 'Cloning Repository'
script {
deleteDir()
git branch: 'branch 이름',
credentialsId: 'Github Credential ID ',
url: 'Github Repository SSH Url(git@github.com로 시작)',
changelog: true,
poll: false,
script: {
dir('branch/가져올 프로젝트 폴더 이름') {
// Additional steps specific to the folder, if needed
echo 'Cloning specific folder: branch/가져올 프로젝트 폴더 이름'
}
}
}
}
post {
success {
echo 'Successfully Cloned Repository'
}
failure {
error 'This pipeline stops here...'
}
}
}
stage('secret.yml download') {
steps {
withCredentials([file(credentialsId: 'Secret File Credential Id ', variable: 'application')]) {
dir('가져올 프로젝트 폴더 이름/') {
script {
if (!fileExists("src/main/resources/") || !isDirectory("src/main/resources/")) {
sh "mkdir -p src/main/resources/"
}
sh "cp \$application src/main/resources/"
}
}
}
}
}
stage('Bulid Gradle') {
steps {
echo 'Bulid Gradle'
dir('가져올 프로젝트 폴더 이름/'){
sh './gradlew clean build -x test'
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Bulid Docker') {
steps {
echo 'Bulid Docker'
dir('가져올 프로젝트 폴더 이름/'){
script {
dockerImage = docker.build imagename
}
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
stage('Push Docker') {
steps {
echo 'Push Docker'
dir('가져올 프로젝트 폴더 이름/'){
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: [sshCredential]) {
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker rm -f [컨테이너 이름] || true'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker ps -q --filter name=[컨테이너 이름] | grep -q . && docker rm -f \$(docker ps -aq --filter name=[컨테이너 이름])'"
sh "ssh -o StrictHostKeyChecking=no [Spring Boot Server username]@[Spring Boot Server IP 주소] 'docker run -d --name [컨테이너 이름] -p 8080:8080 [도커이미지 이름]'"
}
}
post {
failure {
error 'This pipeline stops here...'
}
}
}
}
post {
success {
slackSend (channel: '#채널명', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
failure {
slackSend (channel: '#채널명', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
개선할 점
✅ 특정 폴더 push event에만 동작하도록 설정 필요
위와 같은 파이프라인 설정으로는 하나의 프로젝트 폴더만 변경되더라도 다른 모든 모든 프로젝트 폴더에 push event에 Pipeline이 동작하게 된다. 변경사항이 있는 프로젝트만 동작해야하는데, 나머지 프로젝트들도 동작하면 굉장히 리소스 낭비고 비효율적이다. 이 부분에 대해서는 조금 더 공부하고 수정해서 공유해보도록 하겠습니다!
https://hyeinisfree.tistory.com/23
https://jmlim.github.io/docker/2019/02/25/docker-jenkins-setup/
https://kanoos-stu.tistory.com/55?category=1024343
https://repost.aws/ko/knowledge-center/ec2-memory-swap-file
안녕하세요. 혹시 SSH Username with private key Credentials을 Jenkins에 등록할 때 private key는 젠킨스 컨테이너가 실행중인 VM에 ssh 원격 접속을 할 때 사용하는 key로 등록하는 걸까요?