Github Actions, Jenkins로 CI/CD를 도전해보자! 3편 ( Jenkins CD 도입, 앱서버 EC2에 원격 배포, Webhook 추가하기)

김진형·2024년 7월 31일
0

CI/CD

목록 보기
3/4
post-thumbnail


이 포스트는 'Github Actions, Jenkins로 CI/CD를 도전해보자! 1편Github Actions, Jenkins로 CI/CD를 도전해보자! 2편'의 후속편입니다.

CI/CD를 도입하게 된 계기, 전체적인 구조도, Github Actions를 통한 CI 테스트 내용을 원하신다면 1편을, Jenkins 및 docker 세팅, Jenkins CI 내용을 원하신다면 2편을 참고해주세요.

1. Jenkins CD 도입하기

이번 프로젝트에서는 빌드, 테스트, 배포(환경 변수 설정, 권한 부여, 프로세스 종료 등) 과정을 서버용 EC2에 사람이 접속해서 설정하는 일 없도록 Jenkins로만 제어하고픈 목표가 있었습니다. 왜냐면 서버용 EC2에 접속해서 이것저것 수정하는 작업들(환경변수, 프로세스 종료 등)이 너무 귀찮았기 때문입니다.

2. Jenkins EC2에서 앱서버 EC2로 원격 접속하기 위한 세팅

SSH Plugin인 SSH Agent를 사용하여 Jenkins EC2 -> 앱서버 EC2로 원격 접속을 할 예정입니다. 플러그인 설치 방법은 아래와 같습니다. 저는 이미 설치한 상태라 Installed plugins에 위치합니다. (아마도 이미 설치되어 있을 겁니다!)

주로 PC에서 AWS EC2에 접속을 할 때는 ssh -i {pem key 이름}.pem ubuntu@{ip주소} 와 같은 명령어를 통해 접속하였습니다. (ppk를 이용해 Putty로 접속하는 방법도 있습니다.)

이러한 pem 키를 스크립트에 그대로 적는 것은 보안상 매우매우 위험하기 때문에 Jenkins에 등록하여 사용할 것입니다.
ID는 스크립트에서 사용할 환경변수라고 생각하시면 됩니다.

Key 부분에 AWS EC2의 pem 키를 입력하면 됩니다. pem 키는 EC2 생성할 때 다운로드한 pem 파일에서 가져오시면 됩니다.

-----BEGIN RSA PRIVATE KEY-----
{키 값들}
-----END RSA PRIVATE KEY-----

pem 파일을 메모장으로 열면 위처럼 나올텐데, --- BEGIN --- 과 --- END --- 부분을 포함한 모든 내용을 key에 넣으시면 됩니다.

3. CD 스크립트 작성

Github Actions, Jenkins로 CI/CD를 도전해보자! 2편에서 작성한 CI 스크립트에 이어서 작성합니다.

pipeline {
    agent any
    tools {
        gradle 'gradle'
    }
    environment {
        DB_URL = credentials('db-url-credentials-id')
        DB_USER = credentials('db-user-credentials-id')
        DB_PASS = credentials('db-password-credentials-id')
    }
    stages {
        stage('Git Clone') {
            steps {
                git branch: 'develop', url: 'https://github.com/2024-Saphy/BE.git'
            }
        }
        stage('BE-Build') {
            steps {
                sh "chmod +x ./gradlew"
                sh "./gradlew clean build"
            }
        }
        stage('Send Jar to remote Server') {
            steps {
                sshagent(credentials: ['server-aws-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no ubuntu@x.xx.xxx uptime
                        scp /var/jenkins_home/workspace/Saphy/build/libs/saphy-0.0.1-SNAPSHOT.jar ubuntu@x.xx.xxx:/home/ubuntu/Saphy
                        
                    '''
                }
            }
        }

        stage('Create .env File on remote Server') {
            steps {
                sshagent(credentials: ['server-aws-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no ubuntu@x.xx.xxx "
                        echo 'DB_URL=${DB_URL}' > /home/ubuntu/Saphy/.env
                        echo 'DB_USER=${DB_USER}' >> /home/ubuntu/Saphy/.env
                        echo 'DB_PASS=${DB_PASS}' >> /home/ubuntu/Saphy/.env
                        "
                    '''
                }
            }
        }
        stage('Deploy') {
            steps {
                sshagent(credentials: ['server-aws-key']) {
                    sh '''
                        ssh -o StrictHostKeyChecking=no ubuntu@x.xx.xxx <<'EOF'
                            pid=$(pgrep -f saphy)
                            echo $pid
                            if [ -n "$pid" ]
                            then
                                echo "Stopping process $pid..."
                                kill -15 $pid
                                echo "kill process $pid"
                                sleep 5 
                            else
                                echo "no process"
                            fi
        
                            chmod +x /home/ubuntu/Saphy/saphy-0.0.1-SNAPSHOT.jar
                            chmod +x /home/ubuntu/Saphy/.env
                            cd /home/ubuntu/Saphy
                            sudo nohup java -jar saphy-0.0.1-SNAPSHOT.jar & 
        EOF
                    '''
                }
            }
        }
    }
}

2024/8/7 추가: 자동 배포를 하는 과정에서 kill 프로세스를 해줬음에도 불구하고 8080 포트 충돌이 일어나 서버가 죽는 현상이 자꾸 발생했습니다. 원인을 찾고자 Deploy 부분의 then ~ else 부분을 다음과 같이 변경하고 테스트 중입니다.

then
    echo "Stopping process $pid..."
	kill -15 $pid
    echo "kill process $pid"
    sleep 10
    if kill -0 $pid 2>/dev/null; then
      echo "Process still running. Sending SIGKILL..."
      kill -9 $pid
      echo "Sent SIGKILL to process $pid" 
	fi
else

2024/08/23 추가: 위처럼 수정해도 가끔씩 포트 충돌이 일어나는 현상이 발생했습니다. 그래서!! sleep 10 >> sleep 20으로 충분히 늘려주니 포트 충돌이 안 일어나더라구요. 이건 좀 테스트를 해보면서 적절한 sleep time을 정하는 게 좋아보입니다!

추가된 부분은 다음과 같습니다.

  1. 'Send Jar to remote Server'
    SSH agent 플러그인과 앞서 등록한 server-aws-key(pem) 키를 통해 앱 서버 EC2에 접속합니다. x.xx.xxx는 앱 서버 EC2의 ip 주소입니다.
    이후 scp 명령어를 통해 Jenkins EC2에 존재하는 jar파일을 앱 서버 EC2에 복사합니다.

  2. 'Create .env File on remote Server'
    Jenkins credentioals에 등록한 환경 변수를 앱 서버 EC2에 .env 파일을 만들어 적어줍니다.

  1. 'Deploy'
    앱 서버 EC2에 접속하여 jar파일을 백그라운드로 중단없이 실행합니다. 이때 앞서 실행 중인 서버 프로세스를 종료시키고 새롭게 실행해야 합니다. 그렇지 않다면 아래처럼 포트 충돌이 일어나기 때문에 새롭게 업데이트 된 jar 파일을 실행시킬 수 없고, 앞서 실행한 프로세스만 계속 실행됩니다.

4. 환경변수 오류 해결

CD 스크립트를 다 작성하고 빌드를 했습니다. 그런데 앱 서버 EC2에서 서버를 실행할 때 아래와 같은 DB 세팅 오류가 계속 발생...

.env파일이 생성되지 않아서 발생하는 에러인가 싶어서 확인을 해봤더니 잘 생성이 됨을 알 수 있습니다.

// 디렉토리 구조
└──home
    ├── ubuntu
    │   ├── Saphy
    │   	├── saphy-0.0.1-SNAPSHOT.jar
    │   	├── .env

이후 EC2에 접속해서 home/ubuntu/Saphy 디렉토리에서 nohup java -jar /home/ubuntu/Saphy/saphy-0.0.1-SNAPSHOT.jar &(이하 jar를 실행) 로 실행시키면 분명 잘됐습니다.

근데 jenkins에서 스크립트로 jar를 실행시키면 DB_URL을 못찾는 에러가 발생. 원인을 찾고자 EC2에 접속해서 직접 실험을 해봤습니다.

  1. 먼저 스크립트에 pwd를 추가하여 Jenkins가 jar를 실행시킬 때의 위치를 찾았더니 home/ubuntu였습니다.

  2. 서버용 EC2에 접속하여 home/ubuntu 디렉토리에서 jar를 실행했습니다. 이 디렉토리에서 jar를 실행시키니 바로 DB_URL 에러 발생

  3. 그래서 .env 파일이 위치한 home/ubuntu/Saphy 디렉토리로 이동하여 jar를 실행하니 정상적으로 실행

그렇습니다.... jar 파일 실행 명령어를 입력하는 디렉토리 위치에 .env 파일이 존재해야 이 파일을 참고하여 환경변수를 설정해줄 수 있는 것입니다... (삽질을 너무 많이 했다)

정리하자면,

  1. jar 파일을 실행할 때는 환경변수가 필요합니다.

  2. 저는 환경변수를 .env로 사용했고, jar 파일과 .env 파일의 디렉토리를 같은 장소(home/ubuntu/Saphy)에 위치시켰습니다. 둘의 디렉토리가 같으면 환경 변수를 참조한다고 생각했기 때문입니다.

  3. 그러나 jar 파일을 실행시켰더니 에러가 발생했습니다.

  4. 알고보니 jar 파일 실행 명령어를 입력하는 디렉토리에 .env파일이 있어야 환경변수를 제대로 먹일 수가 있는 것입니다.

  5. home/ubuntu/Saphy 디렉토리에서 jar 파일 실행 명령어를 입력하니 정상작동되었습니다.

결론: jar를 실행시키는 명령어를 입력할 때의 디렉토리 위치에 .env가 존재해야 합니다. 혹시 몰라 다른 디렉토리에서 .env를 만들고 jar를 실행시켜봤더니 역시나 잘 실행됩니다.
그렇기 때문에 'Deploy' step의 마지막 2번째 줄에서 .env파일이 위치한 home/ubuntu/Saphy 디렉토리로 이동하는 명령어를 추가한 것입니다.

5. Github Webhook 추가하기

여태 했던 Pipeline은 Jenkins 내에 '지금 빌드'를 클릭해야 가능했습니다. 이를 PR >> Merge가 됐을 때 자동적으로 수행하여 배포까지 하도록 Github Webhook을 걸어보겠습니다.

5-1. Jenkins에서 설정

이전에 생성한 Item의 Build Triggers 아래 사진처럼 설정해주면 됩니다.

5-2. Github에서 웹훅 추가

원하는 레포지토리의 Settings -> 왼쪽 사이드바의 webhook -> 오른쪽 위 add webhook 클릭
위 사진처럼 젠킨스 접속 주소에 /github-webhook/을 추가하면 모든 과정이 끝납니다!!

이러면 PR -> Merge가 됐을 경우 자동적으로 Jenkins에서 설정한 스크립트를 실행하게 됩니다. 완벽한 자동 빌드, 배포가 이루어 진거죠.

6. 성공

무려 100번의 시도를 통해 성공할 수 있었습니다..!

7. 느낀점

CI/CD를 도입하면서 다음 목표가 생겼습니다.

  1. 앱 서버 또한 docker 컨테이너에 담아서 배포 도전해보기
  2. 무중단 배포 도전해보기

추후 시리즈에 꼭 추가해보도록 하겠습니다!

동시에

퍼블릭 IP랑 포트번호 알면 Jenkins 홈페이지 들어와서 파이프라인 깽판칠 수도 있는 게 아닌가? 다행히 AWS Key라던가 깃허브 Key 등등 중요한 정보는 credential 설정으로 숨길 수 있어도 스크립트는 못 숨기기 때문에 위험할 수도 있지 않나??

라는 궁금증이 생겼습니다.

생각해보니 Jenkins 계정으로 로그인해야하니 보안에는 문제가 없네요 ㅎㅎ...

참고 블로그:
[DevOps] Jenkins를 통한 CI/CD 구축기 1편 (Jenkins 설치)
[DevOps] Jenkins를 통한 CI/CD 구축기 2편 (Backend CI/CD 구축)
ec2 스프링 빌드 시 멈춤 현상 해결법

0개의 댓글