[Infra] 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
        명령어를 사용하여 스크립트를 실행할 때 명령어 앞에 환경 변수를 설정하면, 그 스크립트 내에서는 해당 환경 변수들을 사용할 수 있습니다. 오해를 불러일으켜 죄송합니다.
  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개의 댓글