jenkins,Docker,nginx 를 이용한 Blue/Green 무중단 배포

정 승 연·2024년 4월 3일
1

Todo

  • jenkins,Docker,nginx 를 이용한 Blue/Green 방식 무중단 배포

jenkins,Docker,nginx 를 이용한 Blue/Green 무중단 배포

/home/ubuntu/deploy/ 주요 파일 내용

# deploy.sh
# 0
sh  /home/ubuntu/deploy/copyLogs.sh

EXIST_BLUE=$(docker ps --filter name=${IMAGE_NAME}-blue --filter status=running -q)

# 1
if [ -z "$EXIST_BLUE" ]; then
    echo "Blue Up!"

    docker-compose -p ${IMAGE_NAME}-blue --env-file ./spring.env -f /home/ubuntu/deploy/docker-compose.blue.yaml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
    BEFORE_PORT_NUMBER=8086
    AFTER_PORT_NUMBER=8085
else
    echo "Green Up!"
    docker-compose -p ${IMAGE_NAME}-green --env-file ./spring.env -f /home/ubuntu/deploy/docker-compose.green.yaml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
    BEFORE_PORT_NUMBER=8085
    AFTER_PORT_NUMBER=8086
fi

echo "${AFTER_COMPOSE_COLOR} server up(port:${AFTER_PORT_NUMBER})"

# 2
for cnt in $(seq 1 10)
do
    echo "서버 응답 확인중..(${cnt}/10)";
    UP=$(curl -s http://localhost:${AFTER_PORT_NUMBER}/actuator/health | grep 'UP')
    if [ -z "${UP}" ] 
        then
	    sleep 10
	    continue       
        else
            break
    fi
done

if [ $cnt -eq 10 ]
then
    echo "서버가 정상적으로 구동되지 않았습니다."
    exit 1
fi

CONTAINER=$(docker ps -q -f "name=nginx")
# 3
sudo sed -i "s/${BEFORE_PORT_NUMBER}/${AFTER_PORT_NUMBER}/"  /home/ubuntu/deploy/service-url.inc

docker cp /home/ubuntu/deploy/service-url.inc $CONTAINER:/etc/nginx/conf.d/
echo "Copy url"

docker exec $CONTAINER service nginx reload
echo "reload"

echo "Deploy Completed!!"
# 4
docker stop  ${IMAGE_NAME}-${BEFORE_COMPOSE_COLOR}
docker rm -f  ${IMAGE_NAME}-${BEFORE_COMPOSE_COLOR}
# docker-compose -p ${IMAGE_NAME}-${BEFORE_COMPOSE_COLOR} --env-file ./spring.env -f /home/ubuntu/deploy/docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
echo "$BEFORE_COMPOSE_COLOR server down(port:${BEFORE_PORT_NUMBER})"

yes | docker image prune

curl -X GET -s "https://dev.hprobot.ai/kakao/server-state?state=true&dashboardName=seungyeon"
# jenkins pipeline

pipeline {
    agent any 
    tools{
       gradle 'gradle-7.6.1'
       jdk 'java-17'
   }
   environment{
       repositiory = "seungyeonnnnnni/hprobot:latest"
       DOCKERHUB_CREDENTIALS = credentials('jeongsy-dockerhub')
       dockerImage=''
   }
    stages {
        stage('Github') {
           steps {
                git branch: 'main', credentialsId: 'jeongsy-github', url: 'https://github.com/seungyeonn-i/helper_monitoring_system.git'
           }
        }
        stage('Clean') {
            steps {
                sh 'rm -rf build/libs/*.jar' // 기존 JAR 파일 삭제
            }
        }
        stage('Build') {
           steps {
                sh 'chmod +x ./gradlew'
                sh "./gradlew bootJar"
                sh 'docker build -t $repositiory .'
           }
        }
        stage('Login') {
          steps {
            sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
          }
        }
        stage('Deploy') {
            steps {
                script {
                    sh 'docker push $repositiory'
                }
                dir('build/libs') {
                    sshagent(credentials: ['jeongsy-ssh']) {
                        sh 'ssh -o StrictHostKeyChecking=no ubuntu@43.201.147.248 "sudo docker pull $repositiory"'
                        sh 'ssh -o StrictHostKeyChecking=no ubuntu@43.201.147.248 "sudo IMAGE_NAME=hprobot IMAGE_STORAGE=seungyeonnnnnni BUILD_NUMBER=latest sh deploy/deploy.sh"'
                   }
                }
            }
        }
        
    }
    
}

EC2 폴더 구조

EC2 내에 아래와 같은 파일이 존재해야한다.

.
└── ubuntu
    └── deploy
        ├── application-activate.yml
        ├── application.yml.       
        ├── client_secret.json   
        ├── copyLogs.sh                         // 로그 복사 스크립트
        ├── deploy.sh                           // 실행 파일
        ├── docker-compose.blue.yaml
        ├── docker-compose.green.yaml 
        ├── logback-spring.xml                  // 로그 저장 설정
        ├── profile-application.yml
        ├── **service-url.inc                     // nginx에서 라우팅 할 url**
        └── spring.env           // docker-compose 에서 사용할 환경변수 저장
       
  • 이 파일들은 docker-compose.yaml 의 volumes에 나와있는 사항에 따라 띄워진 docker 내부에서 해당 위치에 복사된다.
    • docker-compose volumes

      Docker Compose 파일에서 volumes 섹션은 컨테이너와 호스트 시스템 간에 파일 시스템을 공유할 때 사용됩니다. 이를 통해 데이터의 지속성과 데이터 공유가 가능하며, 설정 파일이나 코드 등을 컨테이너에 쉽게 제공할 수 있습니다.

       # docker-compose.blue.yaml 일부 
       services:
          volumes:
            - ./application.yml:/app/config/application.yml
            - ./profile-application.yml:/app/config/profile-application.yml
            - ./logback-spring.xml:/app/config/logback-spring.xml
            - ./client_secret.json:/app/config/client_secret.json

jenkins pipeline

  • dev
    pipeline {
        agent any 
        tools{
           gradle 'gradle-7.6.1'
           jdk 'java-17'
       }
       environment{
           repositiory = "seungyeonnnnnni/hprobot:latest"
           DOCKERHUB_CREDENTIALS = credentials('dockerhub')
           dockerImage=''
       }
        stages {
            stage('Github') {
               steps {
                   git branch: 'dev', url: 'https://github.com/@@@/@@@.git', credentialsId: '@@@-jenkins-token'
               }
            }
            stage('Clean') {
                steps {
                    sh 'rm -rf build/libs/*.jar' // 기존 JAR 파일 삭제
                }
            }
            stage('Build') {
               steps {
                    sh 'chmod +x ./gradlew'
                    sh "./gradlew bootJar"
                    sh 'docker build -t $repositiory .'
               }
            }
            stage('Login') {
              steps {
                sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
              }
            }
            stage('Deploy') {
                steps {
                    script {
                        sh 'docker push $repositiory'
                    } 
                    dir('build/libs') {
                        sshagent(credentials: ['dev-spring-boot-ssh-key']) {
                            sh 'ssh -o StrictHostKeyChecking=no ubuntu@!!!.!!.!!.!!! "sudo docker pull $repositiory"'
                            sh 'ssh -o StrictHostKeyChecking=no ubuntu@!!!.!!.!!.!!! "IMAGE_NAME=spring-boot IMAGE_STORAGE=hprobot BUILD_NUMBER=api sudo sh /home/ubuntu/deploy/deploy.sh"'
                       }
                    }
                }
            }
        }
        post {
            failure {
                script {
                    def buildNumber = env.BUILD_NUMBER
                    sh "curl -X GET -s \"https://dev.hprobot.ai/kakao/server-state?state=false&buildNumber=${buildNumber}&dashboardName=dev\""
                }
            }
        }
    }

CI/CD 과정

  1. 포크한 개인 레포 main 브랜치에 머지 ( 트리거 설정 )
  2. jenkins trigger 감지, jenkins 파이프라인 시작
    1. github login
    2. 기존 빌드 파일 삭제 후 build
    3. docker build
    4. docker hub login,push
    5. ec2 서버에 ssh 접속
    6. docker pull
    7. sh ./deploy.sh

docker에 띄운 nginx 구성하기

  1. docker pull nginx
  2. docker exec -it {nginxContainerId} bash
    • nginx docker 내부는 다음과 같이 구성되어야한다.
      그 중 중요한 파일은 application.conf, service-url.inc, nginx.conf
      ```bash
      /etc/nginx
      |-- conf.d
      |   |-- **application.conf**
      |   |-- default.conf
      |   `-- **service-url.inc**
      |-- fastcgi_params
      |-- mime.types
      |-- modules -> /usr/lib/nginx/modules
      |-- **nginx.conf**
      |-- scgi_params
      `-- uwsgi_params
      ```
    • 80으로 들어오는 요청들을 listen 하고 있다가 service-url.inc 로 라우팅
      cat application.conf
      server {
          listen 80;
          include /etc/nginx/conf.d/service-url.inc;
      
          location / {
              proxy_pass $service_url;
          }
      }
      
    • service-url.incdeploy.sh 실행 시 blue/green 컨테이너에 따라 포트번호 바꿔서 작성됨.
      cat service-url.inc
      set $service_url http://{ip address}}:8086; // 현재 8086 포트인 blue 가 띄워져있어서 그럼
    • nginx.conf
      • /etc/nginx/conf.d 의 모든 conf 파일을 include 하고 있음

        cat nginx.conf
        
        user  nginx;
        worker_processes  auto;
        
        error_log  /var/log/nginx/error.log notice;
        pid        /var/run/nginx.pid;
        
        events {
            worker_connections  1024;
        }
        
        http {
            include       /etc/nginx/mime.types;
            default_type  application/octet-stream;
        
            log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                              '$status $body_bytes_sent "$http_referer" '
                              '"$http_user_agent" "$http_x_forwarded_for"';
        
            access_log  /var/log/nginx/access.log  main;
        
            sendfile        on;
            #tcp_nopush     on;
        
            keepalive_timeout  65;
        
            #gzip  on;
        
            **include /etc/nginx/conf.d/*.conf;**
        }

docker network 연결& redis

spring / redis / nginx 모두 컨테이너 간 통신이 존재한다. 따라서 같은 network안에 위치해야한다. 특히 redis 는 dev(test) 에서는 local 로 돌아가고 있고 main 에서는 도커로 띄워서 profile 에 따라 다르게(?) 연결중이다.

docker network

도커 네트워크는 컨네이너-컨테이너, 컨테이너-호스트 간의 통신을 위해 필요하다.

docker network create service-network
docker network connect {network name} {container name}
# docker-compose-@@.yaml 일부

networks:
  default:
    external:
      name: service-network
  • host가 저렇게 설정 되어있다. 같은 네트워크 내에 있는 hprobot-redis 라는 이름을 가진 것을 사용

    # application.yml 일부
      config:
        import: profile-application.yml
        
    # profile-application.yml 일부
    
    ---
    spring:
      redis:
        host: **hprobot-redis**
        port: 6379
      config:
        activate:
          on-profile: blue
    server:
      port: 8085
    logging:
      config: /app/config/logback-spring.xml
    ---
    spring:
      redis:
        host: **hprobot-redis**
        port: 6379
      config:
        activate:
          on-profile: green
    server:
      port: 8086
    logging:
      config: /app/config/logback-spring.xml
    
    ---
    spring:
      redis:
        host: localhost
        port: 6379
      config:
        activate:
          on-profile: dev
    server:
      port: 8080

https://devbksheen.tistory.com/entry/Jenkins-Docker-Nginx-무중단-배포하기

트러블슈팅

docker image prune

docker pull seungyeonnnnnni/hprobot
Using default tag: latest
latest: Pulling from seungyeonnnnnni/hprobot
38a980f2cc8a: Extracting [=================================>                 ]  28.11MB/42.11MB
de849f1cfbe6: Download complete
a7203ca35e75: Downloading [==================================================>]  187.5MB/187.5MB
c9d3047a913b: Download complete
write /var/lib/docker/tmp/GetImageBlob527575243: no space left on device

몇번 pull 하고 나면 docker image 용량이 꽉차서 pull 이 안됐다. 그래서 매번 사용하지 않는 이미지, 컨테이너 지울 수 있도록 명령어 추가

EC2 환경변수

  1. 파이프라인 실행중, IMAGE_NAME,IMAGE_STORAGE 환경변수를 sh 실행전에 넣어주는데 자꾸 환경변수 인식이 안된다는 에러가 떴다.

     IMAGE_NAME=hprobot IMAGE_STORAGE=seungyeonnnnnni BUILD_NUMBER=latest sh deploy/deploy.sh
    • 이렇게 실행할 때 deploy.sh 에서 echo $IMAGE_NAME 하면 아무것도 뜨지 않았다. 그래서 위와 같은 방법으로는 환경변수가 전달 될 수 없다는 결론을 내렸다.
      • IMAGE_STORAGE=seungyeonnnnnni 는 로컬 변수이고 이를 환경 변수로 바꿔주어야한다.
      • 환경변수는 자식 프로세스에게 상속되지만, 로컬 변수는 그렇지 않다. 따라서 sh 파일에서 docker-compose 를 실행하면 그 환경변수가 넘어가지 않는다.
      • chatGPT IMAGE_NAME=hprobot IMAGE_STORAGE=seungyeonnnnnni BUILD_NUMBER=latest sh deploy/deploy.sh 이렇게 실행했을 때 deploy.sh 파일을 실행할 때 환경변수가 할당되지 않았다고 뜨는건 지역 변수는 자식 프로세스에 상속되지 않아서 그런거야? ChatGPT 네, 맞습니다. 스크립트를 실행할 때 IMAGE_NAME, IMAGE_STORAGE, BUILD_NUMBER와 같은 변수들을 명령 줄 앞에 배치하면, 이러한 변수들은 해당 명령이 실행되는 현재의 쉘 세션에 대해서만 환경 변수로 설정됩니다. 즉, 이 변수들은 deploy.sh 스크립트가 실행되는 동안에만 환경 변수로 존재하며, deploy.sh 스크립트 내에서 호출되는 다른 프로그램이나 스크립트로는 상속되지 않습니다. 환경 변수가 자식 프로세스에게 상속되게 하려면, 명령을 실행하기 전에 해당 환경 변수들을 쉘에 명시적으로 내보내야 합니다. export 명령을 사용하면 됩니다. 예를 들면:
        shCopy code
        export IMAGE_NAME=hprobot
        export IMAGE_STORAGE=seungyeonnnnnni
        export BUILD_NUMBER=latest
        sh deploy/deploy.sh
        
        이렇게 하면 deploy.sh 스크립트 내에서도, 그리고 그 스크립트에서 호출하는 어떤 자식 프로세스에서도 IMAGE_NAME, IMAGE_STORAGE, BUILD_NUMBER 환경 변수를 사용할 수 있게 됩니다. 일시적으로만 변수를 설정하고 싶다면, 명령 줄에서 직접 변수를 설정하는 방식으로 실행하는 것도 가능하지만, 이 경우에는 해당 명령어 내에서만 변수들이 유효하며, 이 명령어로 실행되는 스크립트나 프로그램에만 환경 변수가 전달됩니다. 자식 프로세스에서 이러한 변수를 필요로 할 때는 export를 사용해야 합니다. https://lh3.googleusercontent.com/a/AGNmyxaOICM05O7g-k2so1l6JcfTKXoexlQ_oEOfdszc=s96-c 즉, 이 변수들은 deploy.sh 스크립트가 실행되는 동안에만 환경 변수로 존재하며, deploy.sh 스크립트 내에서 호출되는 다른 프로그램이나 스크립트로는 상속되지 않습니다. 그럼
        User
        IMAGE_NAME=hprobot IMAGE_STORAGE=seungyeonnnnnni BUILD_NUMBER=latest sh deploy/deploy.sh
        이렇게 명령을 실행했을 때 deploy.sh 에서 echo $IMAGE_NAME 하면 값이 나온다는거야? ChatGPT 제 설명에 혼동이 있었네요, 정정하겠습니다. 명령 줄에서 IMAGE_NAME=hprobot IMAGE_STORAGE=seungyeonnnnnni BUILD_NUMBER=latest sh deploy/deploy.sh와 같이 실행했을 때, deploy.sh 스크립트 내에서 echo $IMAGE_NAME을 사용하면 환경 변수 IMAGE_NAME의 값인 hprobot이 출력됩니다. 같은 방식으로 IMAGE_STORAGEBUILD_NUMBER 변수에 대해서도 deploy.sh 스크립트 내에서 접근할 수 있습니다. 즉, 이 변수들은 deploy.sh 스크립트가 실행되는 동안에만 환경 변수로 설정되며, 이 스크립트 내에서 사용할 수 있습니다. 하지만, deploy.sh에서 다른 스크립트나 프로그램을 실행할 때, 그 자식 프로세스들에게는 이 환경 변수들이 자동으로 상속되지 않습니다. 이러한 상속을 원한다면, deploy.sh 스크립트 내에서 해당 환경 변수들을 다시 export 해주거나, 자식 프로세스를 호출할 때 명시적으로 환경 변수를 전달해야 합니다. 제가 이전에 한 말이 정확하지 않았습니다. 명령어를 사용하여 스크립트를 실행할 때 명령어 앞에 환경 변수를 설정하면, 그 스크립트 내에서는 해당 환경 변수들을 사용할 수 있습니다. 오해를 불러일으켜 죄송합니다.
  2. 그래서 터미널에서 export 로 선언해두었다.

    • root/ubuntu 별 환경변수가 다른가? 로컬에서 root 로 환경변수 설정하고 Jenkins에서 ubuntu 로 접속해서 테스트 했는데 안됐었다. ubuntu / root 마다 환경변수가 다른가???? 의문

- 서버 재부팅하면 환경변수 사라질 수 있다고 판단하여 영구적으로 설정하였다.
    
    ```bash
    sudo vi /etc/profile
    
    export IMAGE_NAME=hprobot
    export IMAGE_STORAGE=seungyeonnnnnni
    export BUILD_NUMBER=latest
    
    source /etc/profile // 프로파일 파일 실행해 영구적으로 설정
    ```
    

기존에 존재하는 컨테이너 확인

deploy.sh 파일에서 기존에 존재하는 컨테이너를 확인하고 존재하지 않는 컨테이너를 띄운다.

근데 기존 docker-compose 로 존재 여부 판단하는 명령이 안먹혔고

EXIST_BLUE=$(docker-compose -p ${IMAGE_NAME}-blue -f ~/deploy/docker-compose.blue.yaml ps | grep Up)
if [ -z "$EXIST_BLUE" ]; then // EXIST_BLUE 가 null이거나 빈 문자열이라면,
	echo "Blue Up!"

docker 명령어를 사용해 존재 여부 판단하는 것으로 바꿨다. 왜 안됐을까

EXIST_BLUE=$(docker ps --filter name=${IMAGE_NAME}-blue --filter status=running -q)

docker ps 명령은 Docker 엔진에 직접적으로 요청하여 모든 실행 중인 컨테이너를 확인하고, --filter name=${IMAGE_NAME}-blue 옵션을 통해 특정 이름을 가진 컨테이너를 필터링합니다. 이 접근법은 Docker Compose의 프로젝트 구조나 docker-compose.yaml 파일의 위치, 환경변수 설정에 의존하지 않으므로 더 간단하고 직접적인 정보를 제공합니다. 따라서 docker ps를 사용한 방식이 더 일관된 결과를 제공할 가능성이 높습니다.docker ps를 사용한 방식으로 변경한 것은 더 직접적이고 신뢰할 수 있는 접근 방법을 선택한 것입니다.

docker-compose

  • docker-compose @@@.yaml down 명령이 자꾸 안먹었
  • 해당 명령어 단독 실행 시 docker-compose 에서 사용하는 환경변수가 선언되지 않았다는 에러 메시지와 함께 ERROR: invalid reference format 에러가 났다. → Docker 명령어가 잘못된 참조 형식을 갖고 있을 때 발생
  • 그래서 컨테이너 중지, 삭제 를 진행하는 docker-compose down 이 아닌, docker 명령어를 이용해 컨테이너를 중지하고 제거했다.

0개의 댓글