nginx

Nginx Blue green 무중단 배포

MIN우 2024. 4. 27. 14:01
728x90

무중단 배포를 하기 위해서는 몇가지 configuration 설정이 필요합니다.

 

1.  docker-compose.green.yaml

2.  docker-compose.blue.yaml

3.  nginx-docker-compose.yaml

4. mysql-docker-compose.yaml

5. deploy.sh

 

docker-compose.green.yaml

version: "3.7"

services:
  pullo:
    image: kimminwoo1234/pullo-backend:${BUILD_NUMBER}
    restart: always
    ports:
      - 8080:8080
    environment:
      SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
    networks:
      - pullo-network-group
    volumes:
      - ./log:/var/log
networks:
  pullo-network-group: # 기존에 생성한 네트워크 그룹과 동일한 이름으로 지정
    external: true

- BUILD_NUMBER를 환경변수 처리함으로 써 버전관리를 할 수 있도록하였습니다.

- external networks를 사용해야만 compose가 내려가도 네트워크는 사라지지않습니다.

 

docker-compose.blue.yaml 

version: "3.7"

services:
  pullo:
    image: kimminwoo1234/pullo-backend:${BUILD_NUMBER}
    restart: always
    ports:
      - 8081:8080
    environment:
      SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
      SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
      SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
      SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
    networks:
      - pullo-network-group
    volumes:
      - ./log:/var/log
networks:
  pullo-network-group: # 기존에 생성한 네트워크 그룹과 동일한 이름으로 지정
    external: true

- 호스트포트는 8081 게스트포트는 8080으로 돌아가게함으로 blue와 green컨테이너가 동시에 띄워질 수 있도록 설정하였습니다.

 

version: "3.7"
services:
  nginx:
    image: nginx:latest
    restart: always
    ports:
      - 80:80
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
    networks:
      - pullo-network-group
networks:
  pullo-network-group: # 기존에 생성한 네트워크 그룹과 동일한 이름으로 지정
    external: true

 - configuration파일을 불륨마운트를 시켜 제가 직접 custom한 configuration을 주입시켰습니다.

 

경로는 pullo/nginx/conf.d

upstream backend {
    server green_pullo_1:8080;
}

server {
    listen 80;
    
    # HTTP Keepalive Timeout 설정 (기본 75초)
    keepalive_requests 200;

    # 클라이언트 요청 버퍼 크기 설정
    client_body_buffer_size 10K;
    client_header_buffer_size 1k;
    client_max_body_size 8m;
    large_client_header_buffers 2 1k;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        error_page   500 502 503 504  /50x.html;
    }

    location /api/ {
        allow all;

        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
        add_header 'Access-Control-Allow-Headers' 'x-requested-with, authorization, content-type, credential, X-AUTH-TOKEN, X-CSRF-TOKEN';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /swagger-ui/ {

        allow all;
        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
        add_header 'Access-Control-Allow-Headers' 'x-requested-with, authorization, content-type, credential, X-AUTH-TOKEN, X-CSRF-TOKEN';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;


    }
}

- 주위깊게 볼 부분은 upstream쪽입니다 저부분이 바뀌고 nginx -s reload를 통해 nginx의프록시를 변경시켜 라우팅시켜줄 것 입니다.

mysql-docker-compose.yml

version: "3.7"
services:
  mysql:
    image: mysql:5.7
    container_name: pullo-mysql
    environment:
      MYSQL_DATABASE: pullo
      MYSQL_USER: user
      MYSQL_PASSWORD: user
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      MYSQL_CHARSET: utf8mb4
      MYSQL_COLLATION: utf8mb4_general_ci
    ports:
      - 3306:3306
    networks:
      - pullo-network-group
    volumes:
      - ./db:/var/lib/mysql
      - ./initdb:/docker-entrypoint-initdb.d  # Mount a directory containing SQL initialization scripts
networks:
  pullo-network-group: # 기존에 생성한 네트워크 그룹과 동일한 이름으로 지정
    external: true

- spring boot가 띄워질라면 docker mysql이 띄워져야하기에 미리 띄워둡니다.

 

5.deploy.sh

#!/bin/bash
 
# Blue 를 기준으로 현재 떠있는 컨테이너를 체크한다.
EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml ps | grep Up)
 
# 컨테이너 스위칭
if [ -z "$EXIST_BLUE" ]; then
    echo "blue up"
    docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
else
    echo "green up"
    docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yaml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi
 
sleep 30
 
# 새로운 컨테이너가 제대로 떴는지 확인
EXIST_AFTER=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR} -f docker-compose.${AFTER_COMPOSE_COLOR}.yaml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then
    
    # Nginx 설정 변경 및 재로드
    docker exec -it pullo_nginx_1 /bin/bash -c "sed -i 's/${BEFORE_COMPOSE_COLOR}/${AFTER_COMPOSE_COLOR}/g' /etc/nginx/conf.d/nginx.conf && nginx -s reload"
    echo "Nginx configuration updated and reloaded"
    # 이전 컨테이너 종료
    docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
    echo "$BEFORE_COMPOSE_COLOR down"
fi

- blue를 기준으로 그린이 띄워져있는지 블루가 띄워져있는지 체크를 합니다.

- 새로운 컨테이너 제대로 떳으면 컨테이너에 bash shell로 접속하여 nginx.conf파일에서 upstream부분을 새로운 컨테이너 이미지로

변경시켜줍니다.

- nginx는 새로운 그린포트로 업데이트가 될 것이고, 기존에 있던 blue 컨테이너는 삭제시켜줍니다.

 

 

JenkinsFile구성

pipeline {
    agent any
    options {
    timeout(time: 1, unit: 'HOURS') // set timeout 1 hour
    }

    environment {

        TIME_ZONE = 'Asia/Seoul'

        //github
        TARGET_BRANCH = 'develop'
        REPOSITORY_URL= 'https://github.com/minwoo1999/pullo'

        //docker-hub
        registryCredential = 'docker-hub'

        CONTAINER_NAME = 'pullo-backend'
        IMAGE_NAME = 'kimminwoo1234/pullo-backend'
    }



    stages {


        stage('init') {
            steps {
                echo 'init stage'
                deleteDir()
            }
            // post {
            //     success {
            //         echo 'success init in pipeline'
            //     }
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'fail init in pipeline'
            //     // }
            // }
        }

        stage('Prepare') {
            steps {
                echo 'Cloning Repository'
                git branch: 'develop',
                    url: 'https://github.com/minwoo1999/pullo.git',
                    credentialsId: 'gitihub-signin'
            }
            // post {
            //     success {
            //         echo 'Successfully Cloned Repository'
            //     }
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'This pipeline stops here...'
            //     // }
            // }
        }
      // 일단은 merge 하기전에 테스트통과함으로 테스트없이 빌드
        stage('Build Gradle') {
            steps {
                echo 'Build Gradle'

                dir('.'){
                    sh '''
                        pwd
                        cd /var/jenkins_home/workspace/pullo-backend
                        chmod +x ./gradlew
                        ./gradlew build

                    '''
                }
            }
            // post {
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'This pipeline stops here...'
            //     // }
            // }
        }

        // 도커 이미지를 만든다. build number로  latest 태그 부여한다.
        stage('Build Docker') {
            steps {
                echo 'Build Docker'
                sh """
                    cd /var/jenkins_home/workspace/pullo-backend
                    docker build -t $IMAGE_NAME:$BUILD_NUMBER .
                    docker tag $IMAGE_NAME:$BUILD_NUMBER $IMAGE_NAME:latest
                """
            }
            // post {
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'This pipeline stops here...'
            //     // }
            // }
        }


     // 빌드넘버 latest
        stage('Push Docker') {
            steps {
                echo 'Push Docker'
                script {
                    docker.withRegistry('', registryCredential) {
                        docker.image("${IMAGE_NAME}:${BUILD_NUMBER}").push()
                        docker.image("${IMAGE_NAME}:latest").push()
                    }
                }
            }
            // post {
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'This pipeline stops here...'
            //     // }
            // }
        }



    stage('rm container and rm images') {
            steps {
                echo 'rm container stage'
                sh '''
                docker rm -f $CONTAINER_NAME
                docker image prune -f --filter "label=${IMAGE_NAME}"
                '''
            }
            // post {
            //     success {
            //         echo 'success rm container in pipeline'
            //     }
            //     // failure {
            //     //     slackSend (channel: '#backend', color: '#FF0000', message: "${env.CONTAINER_NAME} CI / CD 파이프라인 구동 실패, 젠킨스 확인 해주세요")
            //     //     error 'fail rm container in pipeline'
            //     // }
            // }
    }

    stage('Docker run') {
            steps {
                echo 'Pull Docker Image & Docker Image Run'

                script {
                    docker.withRegistry('', registryCredential) {
                        sshagent (credentials: ['ssh']) {
                            sh "ssh -o StrictHostKeyChecking=no ubuntu@43.200.143.218 'cd pullo && sed -i \"s/^BUILD_NUMBER=.*/BUILD_NUMBER=${BUILD_NUMBER}/\" .env'"
                            sh "ssh -o StrictHostKeyChecking=no ubuntu@43.200.143.218 'cd pullo && ./deploy.sh'"
                        }
                    }
                }

            }
            post {
                    failure {
                      echo 'Docker Run failure !'
                    }
                    success {
                      echo 'Docker Run Success !'
                    }
            }
        }



    stage('Clean Up Docker Images on Jenkins Server') {
        steps {
            echo 'Cleaning up unused Docker images on Jenkins server'

            // Clean up unused Docker images, including those created within the last hour
            // sh "docker image prune -f --all --filter \"until=1m\""
            sh "docker image prune"
        }
    }




}

    // post {
    //     success {
    //         slackSend (channel: '#backend', color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
    //     }
    //     failure {
    //         slackSend (channel: '#backend', color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
    //     }
    // }
}

 

- 환경변수 부분에서 BUILD_NUMBER를 변경하고

- ./deploy.sh를 실행시켜 blue/green배포를 실시합니다.

- docker image prune 명령어를 통해 사용하지 않는 이미지들은 처리해줍니다.

 

 

nginx swagger config

upstream backend {
    server blue_pullo_1:8080;
}

server {
    listen 80;
    
    # HTTP Keepalive Timeout 설정 (기본 75초)
    keepalive_requests 200;

    # 클라이언트 요청 버퍼 크기 설정
    client_body_buffer_size 10K;
    client_header_buffer_size 1k;
    client_max_body_size 8m;
    large_client_header_buffers 2 1k;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        error_page   500 502 503 504  /50x.html;
    }

    location /api/ {
        allow all;

        add_header 'Access-Control-Allow-Origin' "$http_origin";
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, PATCH, DELETE';
        add_header 'Access-Control-Allow-Headers' 'x-requested-with, authorization, content-type, credential, X-AUTH-TOKEN, X-CSRF-TOKEN';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /swagger-ui/ {

        allow all;

        if ($request_method = 'OPTIONS') {
            return 204;
        }

        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;


    }

    location /v3/ {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

참고한 블로그:https://jay-ji.tistory.com/99

728x90