Gitlab, Jenkins, Docker, Docker Hub, Nginx, Blue-Green 무중단배포 CI/CD 구축 - 2

2.5*2 하빈·2024년 5월 23일
21
post-thumbnail

전 과정이 궁금하시다면?
Gitlab, Jenkins, Docker, Docker Hub, Nginx, Blue-Green 무중단배포 CI/CD 구축 - 1

  • 자 이제 Jenkins Script와 Docker, Docker Hub를 이용해서 서버를 배포해볼까요?!

지금까지 과정이 많이 힘드셨죠?

생각지도 못한 에러도 많고잉 하지만 우리는 계속 나아가야죠?


1 - Docker Hub Setting

Login > Create repository > name setting > create



2 - Docker file 작성

  • 이제 프로젝트 안에 Docker File 생성할까요?

현재 프로젝트 구조상 backEnd 폴더
우측 클릭하면 Dockerfile 생성할 수 있습니다
알죠? 다들 알지요?~!!


2-1 Docker file 내용

설명은 주석으로 했으니까 알아서들 참고하시고~

혹시 수동 배포가 궁금하다면?
AWS 서버 - Spring Boot 띄우기 (2-.jar)

# open jdk 21 버전의 환경을 구성
FROM openjdk:21

# tzdata 패키지 설치 및 타임존 설정
RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && echo Asia/Seoul > /etc/timezone

# build가 되는 시점에 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언
# build/libs - gradle로 빌드했을 때 jar 파일이 생성되는 경로
ARG JAR_FILE=build/libs/ourClass-0.0.1-SNAPSHOT.jar

# JAR_FILE을 agaproject.jar로 복사
COPY ${JAR_FILE} ourClass.jar

# 운영 및 개발에서 사용되는 환경 설정을 분리
# -Duser.timezone=Asia/Seoul JVM 옵션을 사용하여 애플리케이션 수준에서도 타임존을 설정
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=dev", "-Duser.timezone=Asia/Seoul", "/ourClass.jar"]

3 - Jenkins Credential Setting

JASYPT를 사용하신다면 ?
: Secret text 등록

Docker Hub 사용하신다면 ?
: Username with password 등록

EC2 Server IP도 감추고 싶다면?
: Secret text 등록

Credential 등록까지.. 또 알려줘야해?

글쓴이 양반 일 제대로 하라구...

네... 알려드리겠습니다...


Docker Hub 설정
Username : Docker Hub ID
Password : Docker Hub PW
ID : pipline Script 에서 사용할 이름 = DOCKER_USER

DOCKER Repository 설정
Username : Docker Hub nameSpace
Password : Docker Hub RepositoryName
ID : pipline Script 에서 사용할 이름 = DOCKER_REPO


Jasypt Key 설정
Secret : Key 값
ID : pipline Script 에서 사용할 이름 = JASYPT_KEY

EC2 IP 설정
Secret : 서버 주소
ID : pipline Script 에서 사용할 이름 = EC2_SERVER_IP


4 - SSH 접속을 위한 설정

Dashboard > Jenkins 관리 > Plugins > Available plugins > 검색 : [ssh agent] > download

  • 저는 이미 다운 받아서 없어요

Jenkins Credentials 로 돌아가서~!!!

Kind : SSH Username with private key
Scope : Global
ID : my-ssh-credentials
Description : ssh connect
Username : ubuntu
Private Key > Enter directly Check > Add
.pem (key 내용 붙여넣기)


뭐? .pem 키 내용을 붙여넣으라고?

그게 뭔데 씹덕아?

이것도 알려드립죠..


  • .pem Key File이 있는 곳으로 가서
    Bash 열고
    vi [내 키 파일 이름].pem


  • BEGIN 부터 END 끝까지 복사합니다! 반드시 끝까지!!


  • 복사한 내용을 붙여넣으세욥!


5 - Nginx 설정 변경

이제 EC2로 접속해서 무중단 배포를 위한 Nginx 설정을 변경해볼까요?

거의 다 왔다!!!!!!

캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ
캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ
캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ캬캬캬캬캬ㅑㅋㅁ

글쓰는 것도 참 힘드네요..

자! 조금만 더 힘내봅시다!


  • EC2 접속하시고!
    아 근데 진짜 EC2 접속하는 방법은... 못알려줘요
    이거 모르면 ...크흠... 아닙니다 여튼 접속해보자구요


# 설정 파일 열기
sudo vim /etc/nginx/sites-enabled/default
  • server_name _;
    밑에 아래 명령어 추가하시구요~

  • location / { } 밑에 backEnd context path 경로 추가하세요!

저는 /api 뒤로 들어오는 요청은 다 Back Restful API 요청으로 받으려고 이렇게 설정했어요

여기서 잠깐! 이건 알고가야지?

proxy_pass 지시어란?
주로 리버스 프록시 설정에서 사용됩니다.
proxy_pass 지시어를 사용하면 특정 요청을 다른 서버로 전달하고,
그 서버의 응답을 클라이언트에게 다시 전송할 수 있습니다.

이를 통해 요청과 응답을 중간에서 조작할 수 있으며
부하 분산, 콘텐츠 캐싱, 보안 개선 등 여러 가지 이점을 누릴 수 있습니다.

그래서?
Nginx 서버에 들어오는 /api로 시작하는 모든 요청을
$service_url 변수에 저장된 URL로 전달합니다.

여기서 $service_url은 동적으로 설정될 수 있는 변수이며,
이 변수의 값은 다른 서버의 URL이 될 수 있습니다.
예를 들어, $service_url이 http://backend1.example.com으로 설정되어 있다면,
/api로 들어온 모든 요청은 http://backend1.example.com으로 전달된다.


  • service-url.inc 파일 추가하기
sudo vim /etc/nginx/conf.d/service-url.inc
  • service-url.inc 파일에 아래 명령어 추가
set $service_url http://127.0.0.1:8080;

나중에 이 파일에 있는 port를 배포 스크립트에서 변경하면서 무중단 배포를 진행합니다.


  • nginx 설정 적용
sudo service nginx restart
sudo systemctl reload nginx

6 - Jenkins pipline 작성

  • 이제 젠킨스에서 pipline 작성을 해볼까요?
pipeline {
	agent any
	
    tools {
        jdk ("jdk21")
    }
	
	stages {
	    stage('Git Clone'){
            steps {
                git branch: 'main', credentialsId: 'GitLab_Login', url: 'https://[gitlabUrl]/[Project Directory]/[ProjectName].git'
            }
            post {
                failure {
                  echo 'Repository clone failure !'
                }
                success {
                  echo 'Repository clone success !'
                }
            }
	    }
	    stage('Build') {
			steps {
        		//프로젝트 권한 변경
        		sh 'chmod +x ./backEnd/gradlew'
        		//프로젝트 빌드
        		withCredentials([string(credentialsId: 'JASYPT_KEY', variable: 'JASYPT_KEY')]) {
        		    sh 'cd ./backEnd && ./gradlew clean build -PJASYPT_KEY=$JASYPT_KEY'
        		}
			}
		}
		stage('Docker Hub Login'){
		    steps{
			    withCredentials([usernamePassword(credentialsId: 'DOCKER_USER', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
                    sh 'echo "$DOCKER_PASSWORD" | docker login -u $DOCKER_USERNAME --password-stdin'
                }
		    }
		}
		stage('Docker Build and Push') {
			steps {
				withCredentials([usernamePassword(credentialsId: 'DOCKER_HUB', passwordVariable: 'DOCKER_PROJECT', usernameVariable: 'DOCKER_REPO')]) {
                    sh 'cd ./backEnd && docker build -f Dockerfile -t $DOCKER_REPO/$DOCKER_PROJECT .'
				    sh 'cd ./backEnd && docker push $DOCKER_REPO/$DOCKER_PROJECT'
				    echo 'docker push Success!!'
                }
				echo 'docker push Success!!'
			}
		}
		stage('Deploy') {
			steps {
				sshagent(credentials: ['my-ssh-credentials']) {
                    withCredentials([string(credentialsId: 'EC2_SERVER_IP', variable: 'IP')]) {
                        sh 'ssh -o StrictHostKeyChecking=no ubuntu@$IP "sudo sh deploy.sh"'
                    }
                }
			}
		}
	}
}

아! 그리고 pipline은 개인 프로젝트 폴더나 설정에 맞게끔 변경하셔야합니다.

이거는 제가 뭘 알려드릴 수 없어요..
Log 보고 하나하나씩 잘 해결하셔요

이 정도는 해결하셔야합니다.


  • 우선 저는 JDK 21 사용해서 따로 Jenkins Tools 에서 빌드되도록 설정했습니다.

JDK 21 Build 방법
아래 링크를 참조해주세요!
Jenkins JDK 21 빌드하기!


  • 참고사항

StrictHostKeyChecking=no 옵션은 SSH 클라이언트가 서버에 처음 연결할 때 서버의 호스트 키를 검사하는 과정을 건너뛰게 합니다.

StrictHostKeyChecking=no는 주로 자동화된 스크립트나 배포 프로세스에서 사용됩니다.
이러한 상황에서는 호스트 키 확인 과정이 자동화 작업을 방해할 수 있으므로,
명시적으로 이 옵션을 사용하여 스크립트가 사용자 개입 없이 원활하게 실행되도록 할 수 있습니다.


7 - Deploy File 작성

  • 이제 EC2 환경에서 배포 스크립트를 작성해봅시다!
sudo vim deploy.sh

#deploy.sh

#0
# 이미지 갱신
sudo docker compose -p ulvan-8080 -f /home/ubuntu/docker-compose.ulvan8080.yml pull
sudo docker compose -p ulvan-8081 -f /home/ubuntu/docker-compose.ulvan8081.yml pull

#1
EXIST_GITCHAN=$(sudo docker compose -p ulvan-8080 -f docker-compose.ulvan8080.yml ps | grep Up)

if [ -z "$EXIST_GITCHAN" ]; then
        echo "8080 컨테이너 실행"
        sudo docker compose -p ulvan-8080 -f /home/ubuntu/docker-compose.ulvan8080.yml up -d --force-recreate
        BEFORE_COLOR="8081"
        AFTER_COLOR="8080"
        BEFORE_PORT=8081
        AFTER_PORT=8080
else
        echo "8081 컨테이너 실행"
        sudo docker compose -p ulvan-8081 -f /home/ubuntu/docker-compose.ulvan8081.yml up -d --force-recreate
        BEFORE_COLOR="8080"
        AFTER_COLOR="8081"
        BEFORE_PORT=8080
        AFTER_PORT=8081
fi

echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"

# 2
for cnt in `seq 1 10`;
do
    echo "서버 응답 확인하는중~(${cnt}/10)";
    UP=$(curl -s http://127.0.0.1:${AFTER_PORT}/api/health-check)
    if [ "${UP}" != "OK" ]; then
        sleep 10
        continue
    else
        break
    fi
done

if [ $cnt -eq 10 ]; then
    echo "서버에 문제가 있어요..."
    exit 1
fi

# 3
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"

# 4
echo "$BEFORE_COLOR server down(port:${BEFORE_PORT})"
sudo docker compose -p ulvan-${BEFORE_COLOR} -f docker-compose.ulvan${BEFORE_COLOR}.yml down

# 5
sudo docker image prune -f
---

!!!!!!!필수 참고!!!!!

#2 부분에 curl -s http://127.0.0.1:${AFTER_PORT}/api/health-check 라인 때문에
/api/health-check 엔드포인트에 해당하는 API 를 설계해야함
반환은 if [ "${UP}" != "OK" ]; then 명령어 때문에 OK 를 반환하면 된다.

@RestController
public class HealthCheckController {
    @GetMapping("/health-check")
    public String healthCheck() {
        return "OK";
    }
}

8 - Docker-compose File 작성

  • 이제 Docker Compose로 서버를 띄울 수 있도록 파일을 작성해봅시다.

    저희는 8080 Port, 8081 Port 2가지를 쓰면서 무중단 배포를 하니까
    docker compose file도 2개가 있어야겠죠?

  • docker compose 파일 생성
sudo vim docker-compose.ulvan8080.yml

  • docker-compose.ulvan8080.yml 파일 작성
version: '3.1'

services:
  api:
    image: [docker hub namespace]/[docker hub repositoryName:latest]
    container_name: ulvan-8080
    environment:
      - TZ=Asia/Seoul
      - LANG=ko_KR.UTF-8
      - HTTP_PORT=8080
      - jasypt.encryptor.key=[KEY VALUE]
    ports:
      - '8080:8080'

  • docker compose 파일 생성
sudo vim docker-compose.ulvan8081.yml

  • docker-compose.ulvan8081.yml 파일 작성
version: '3.1'

services:
  api:
    image: [docker hub namespace]/[docker hub repositoryName:latest]
    container_name: ulvan-8081
    environment:
      - TZ=Asia/Seoul
      - LANG=ko_KR.UTF-8
      - HTTP_PORT=8081
      - jasypt.encryptor.key=[KEY VALUE]
    ports:
      - '8081:8080'

참고로 JASYPT_KEY 설정은 아래 링크를 참조해주세요!
JASYPT를 이용하여 application.yml 파일 암호화 하기


  • 이제 Test 해보세요!!
    길고 긴 여정이 마무리 되었습니다.

    이 글을 읽고 있는 모든 분들이 도움이 되었으면 합니다.

이 빌드 과정 모든 것이 초록불!@!!!@@!@!

엄청난 희열...


긴글 읽어주셔서 감사합니다!

우리 모두 이제 인프라 고수입니다^^



TIP : 메타모스트 채널 알림 연동은 아래 링크를 참조해주세요!
Jenkins Mattermost 채널 알림 연동하기

profile
끝내주는 남자

2개의 댓글

comment-user-thumbnail
2024년 6월 7일

ㅋㅋㅋ 김연아 짤 너무 웃기네요

1개의 답글