백엔드 Backend/서버 Server

Git + Jenkins + Docker로 서버와 CI/CD구축하기

달래dallae 2024. 8. 2. 10:42

0️⃣들어가기 전

 

이 글은 프로젝트를 완성한 뒤, 프로젝트를 직접 배포하는 과정을 기록하는 글입니다.

위와 같이 Ec2로 서버를 구성하고 Github, Jenkins, Docker를 사용해 CI/CD를 구축해보겠습니다.

현재 Spring boot 서버로 통합하여 구현하긴 했지만, mysql과 swagger 서버도 각자 분리하는 것이 더 바람직합니다.

 

✔️ 플로우

Jenkins 서버

Git clone → Gradle 빌드 → Dockerfile로 도커 이미지를 빌드 → Docker Hub에 Image Push

Spring boot 서버

Docker Image pull → docker compose up

 


1️⃣ 인스턴스 생성

jenkins 서버와 개발서버(spring boot 서버)를 위해 총 2가지 인스턴스를 생성해야 합니다.

인스턴스를 생성한 뒤 포트를 허용해주기 위해서 각 인스턴스마다 인바운드 규칙을 수정해주어야 합니다.

1. 인바운드 규칙 및 용량 설정

  • Jenkins 인스턴스
    • 보안그룹의 인바운드 규칙에 사용자 지정 TCP로 8080(젠킨스 포트) 추가
    • storage 용량을 30gb로 (build시 용량 부족오류 발생 방지)
  • Spring boot 인스턴스
    • 보안그룹의 인바운드 규칙에 사용자 지정 TCP로 8080(spring boot 포트) 추가

 

2. 인스턴스 접속

인스턴스에 접속하려면, pem키를 발급받은 뒤 사전 설정을 해야합니다.

 

✔️ pem(키페어)의 권한 변경

$ chmod 600 {키페어파일}
  • chmod: 파일 모드 변경 (읽기/쓰기/실행)
  • 600 : 나에게만 읽기/쓰기 권한

✔️ pem으로 인스턴스 접속

$ ssh -i {키 페어 파일 경로} {사용자 이름}@{인스턴스의 IPv4 주소나 도메인}

 


2️⃣ Jenkins 서버 설정

pem키를 통해 인스턴스에 접속한 뒤 다음 설정을 진행합니다.

1. 필요 패키지 설치

$ sudo apt update
$ sudo apt upgrade
$ sudo apt install build-essential
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common

 

2. docker 설치

$ sudo wget -qO- <https://get.docker.com/> | sh
$ sudo systemctl start docker
$ sudo systemctl enable docker

 

✔️docker group 설정

sudo를 사용하지 않고 docker 명령어를 사용하기 위해서는 현재 사용자계정을 docker group에 포함시켜주면 됩니다. sudo는 관리자의 권한을 사용하기 위한 명령어이므로, 꼭 필요한 계정에만 docker group을 설정해주어야 합니다.

$ sudo usermod -aG docker ${USER}
$ id -nG

 

3. jenkins 설치

$ docker pull jenkins/jenkins:lts
$ 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
  • ssh 설정을 위해 볼륨을 마운팅해놓습니다.

 

4. jenkins 접속

1) 브라우저에서 {ec2 IPv4주소}:8080 에 접속합니다.

접속하면 비밀번호를 입력하는 창이 나타납니다.

$ docker logs jenkins

비밀번호를 입력한 뒤, install suggested plugins를 클릭해 설치합니다.

 

2) 계정 생성

계정 생성 후 메인보드가 나타나면 성공입니다.

 


3️⃣ Jenkins와 Github 연동

1. SSH Key 생성

Jenkins 외부에서 서버에 접근할 수 있도록 public key와 private key를 설정해주어야합니다.

SSH 키에 대한 설명은 다음에서 간단하게 정리하였습니다. SSH Key 동작원리

 

✔️ SSH Key 생성

$ ssh-keygen 
  • Container 밖에서 ssh 키를 생성하면 Jenkins Container를 생성할 때 미리 마운팅해놓았던 볼륨 경로에 연결됩니다. (호스트 /home/ubuntun/.ssh → 컨테이너 /root/.ssh)
  • EC2에 접속하면 기본 유저가 ubuntu이기 때문에 /home/ubuntu/.ssh에 프라이빗키(id_rsa 또는 id_ed25519)와 퍼블릭키(id_rsa.pub 또는 id_ed25519.pub)가 생성됩니다.

✔️ SSH Key 확인

$ cat /home/ubuntu/.ssh/id_rsa.pub 또는 cat id_ed25519.pub # public key 확인
$ cat /home/ubuntu/.ssh/id_rsa 또는 cat id_ed25519         # private key 확인

 

2. github에 Jenkins public key를 deploy key로 등록

✔️ github에서 등록

  • github repository → settings → deploy keys → add deploy key에 접속합니다.
  • key 부분에 위 명령어를 통해 확인한 ssh public key를 입력합니다.

 

3. Jenkins에 github의 credentials 등록

github에서 public key를 등록했으므로, Jenkins 서버에서 private key를 등록해 정보를 가져올 수 있도록 합니다.

 

✔️ jenkins에서 github이 사용할 private key 등록

  • jenkins 관리 → security → credentials에 접속합니다.
  • Store Jenkins에 Domain이 (global)인 화살표를 눌러 Global credentials (unrestricted)로 이동합니다.
  • 왼쪽 메뉴의 Add credentials를 눌러 credentials를 추가합니다.
    • kind: SSH Username with private key
    • id: pipeline 작성 시 CredentialsId로 식별할 수 있는 값을 설정합니다. 저는 github으로 등록했습니다.
    • private key에 enter directly 체크 Jenkins 서버의 SSH private key를 입력합니다.

 


4️⃣ Jenkins와 docker hub 연동

1. Jenkins에서 docker, docker pipeline 플러그인 설치

jenkins 관리 → plugins → available plugins → docker, docker pipeline을 설치합니다.

 

2. Jenkins에서 docker hub의 credentials 등록

jenkins 관리 → security → credentials에 접속합니다. docker hub에 대한 정보가 필요하기때문에, 없다면 가입이 필요합니다.

  • kind: Username with password
  • username: 본인 docker hub의 username(email이 아닙니다!)
  • id: pipeline에서 식별할 수 있는 값을 입력합니다. 저는 dockerhub로 등록하였습니다.
  • password: 본인 docker hub의 비밀번호

 

3. Jenkins 컨테이너 내부에서 docker.sock 권한 추가

pipeline을 작성하면 Jenkins 컨테이너 내부에서 docker 명령어를 사용해야 하는데, 컨테이너 내부에는 docker가 설치되어있지 않습니다.

이 때 docker container에서 또 docker container를 추가하기보다는, 호스트인 container의 docker.sock의 권한만 추가하면 사용할 수 있습니다.

 

✔️ 볼륨 마운트 확인

이는 Jenkins container를 가동할 때 아래의 명령어처럼 외부 docker volume을 마운트해놓았기 때문에 가능합니다.

$ 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

 

✔️ docker.sock 접근 허용

아래의 명령어로 Jenkins container에 접속한 뒤 docker.sock에 대한 접근을 허용해줍니다.

$ docker exec -it jenkins bash

# sudo 설치 (docker 컨테이너에서는 sudo 명령어를 따로 설치해주어야합니다)
$ apt-get update && apt-get install -y sudo

$ adduser --disabled-password --gecos "" user  \\
    && echo 'user:user' | chpasswd \\
    && adduser user sudo \\
    && echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

$ sudo chmod 666 /var/run/docker.sock

 

✔️ 잘 안되는 경우

하지만 저는 이게 잘 먹히지 않아서 container 내부에 docker를 설치해주었습니다. 참고: 2️⃣ Jenkins 서버 설정

 

 


5️⃣ Jenkins 와 Spring boot Server 연동

Jenkins서버가 Spring Boot 서버에서 도커 이미지를 Pull해서 실행하게 하기 위해 필요합니다.

Jenkins Pipeline Script에서 SSH를 사용하여 Spring Boot Server의 명령어 실행을 할 수 있도록 합니다.

1. Jenkins에서 SSH Agent 플러그인 설치

2. Spring Boot 서버에서 Jenkins public key 등록

Spring boot에서 Jenkins서버의 접속을 허용해주기 위해서 필요합니다.

# Jenkins Server에서 public key 확인
$ cat /home/ubuntu/.ssh/id_ed25519.pub

# Spring Boot Server에 Jenkins Server public key 추가
$ vi /home/ubuntu/.ssh/authorized_keys

원래 있던 authorized_keys 내용을 건드리면 ssh 접속 오류가 날 수 있으므로 주의해야합니다.

더보기

잘 되던 pem키로 접속이 안되는 이슈 발생중간에 빌드하면서 자꾸 실패해서그런지, 잘 접속되던 pem key로 접속하려고 하니까 Permission Denied(Public Key) 오류가 발생했습니다.

authorized_keys도 다시 변경해보고 새로 keygen을 발급해서 변경해봐도 효과가 없던 찰나에, 아래처럼 권한을 다시 주니까 성공했습니다.

 

시도1. 파일 재생성 & 복사

$ echo 'ssh-rsa {프라이빗키}' >> /home/ubuntu/.ssh/authorized_keys

 시도2. 권한 부여 -> 성공

$ sudo chown root:root /home
$ sudo chmod 755 /home
$ sudo chown ubuntu:ubuntu /home/ubuntu -R
$ sudo chmod 700 /home/ubuntu /home/ubuntu/.ssh
$ sudo chmod 600 /home/ubuntu/.ssh/authorized_keys

참고 : https://repost.aws/knowledge-center/ec2-linux-fix-permission-denied-errors

 

2. Jenkins에 Spring Boot서버 SSH 등록

앞서 github Credentials를 등록했던것과 마찬가지로 등록합니다. (참고: ✔️ jenkins에서 github이 사용할 private key 등록)

동일한 Credentials를 사용해도 되지만, 서비스를 구분하기 위해 추가로 등록하였습니다.

 


6️⃣ Jenkins와 Git webhook 연동

Git의 특정 repository에서 push event가 발생했을 때 Jenkins build가 실행되게끔 구현하기 위해서 pipeline과 github webhook을 연동해야합니다.

1. Jenkins에서 Github Integration Plugin 설치

Jenkins 대시보드 > Jenkins 관리 > 플러그인 관리 > 설치 가능 > Github Integration 플러그인을 검색하고 설치 및 재실행합니다.

 

2. Jenkins pipeline 설정

  • Github project 설정Pipeline 구성 화면 > General 영역에서 Github project를 선택한다. Project url에 본인의 Github Repository Url을 입력한다. 이 때 Repository Url은 Clone 시 사용하는 HTTPS Url(.git으로 끝남)을 입력합니다.
  • Build Triggers 설정Pipeline 구성 화면 > Build Triggers 영역에서 GitHub hook trigger for GITScm polling을 선택합니다.

 

3. Github Webhook 추가

Github Repository에서 Settings > Webhooks > Add Webhook 을 눌러 Webhook을 추가합니다.

  • Payload URL {Jenkins Server URL}:{Jenkins Server 포트}/github-webhook/
  • Content type application/x-www-form-urlencoded
  • 나머지는 모두 default 설정 유지 Add webhook 버튼을 눌러 Webhook을 추가 → 목록에서 녹색 체크 아이콘이 생성되면 성공입니다.

 


7️⃣ Jenkins Pipeline 작성

pipeline {
    agent any

    environment {
        imagename = '{docker hub에 올린 spring boot image}'
        registryCredential = 'dockerhub'
        dockerImage = ''
    }

    stages {
        stage('Git Clone') {
            steps {
                echo 'Cloning Repository'
                git url: 'git@github.com:{git ssh주소}',
                    branch: 'prd',
                    credentialsId: 'github'
            }
            post {
                success {
                    echo 'Successfully Cloned Repository'
                }
                failure {
                    error 'This pipeline stops here...'
                }
            }
        }

        stage('Build Gradle') {
            steps {
                echo 'Build Gradle'
                dir('.') {
                    sh 'chmod +x gradlew'
                    sh './gradlew clean build -x test'
                }
            }
            post {
                failure {
                    error 'This pipeline stops here...'
                }
            }
        }

        stage('Build Docker') {
            steps {
                echo 'Build 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']) {
                    sh "ssh -o StrictHostKeyChecking=no ubuntu@{서버IP} 'docker pull ${imagename}'"
                    sh "ssh -o StrictHostKeyChecking=no ubuntu@{서버IP} 'docker compose -f /home/ubuntu/spring/compose/docker-compose.yml up --build -d'" 
                    sh "ssh -o StrictHostKeyChecking=no ubuntu@{서버IP} 'docker image prune -f'" 
                }
            }
        }
    }
}
  • docker-compose로 docker를 빌드합니다.
  • 컨테이너의 볼륨 설정을 통해서 application.yml을 따로 관리할 수 있도록 설정하였습니다.