Blue Green 무중단 배포

‍서지오·2023년 9월 17일
0

클라우드

목록 보기
2/2

0. 동작 과정

  1. 깃 랩 webhook을 통해 내가 지정한 branch에 push event 발생 시 젠킨스가 감지
  2. 젠킨스 서버로 gradle 사용해서 build(.jar 파일 생성)
  3. publish over ssh로 젠킨스 서버 내 .jar 파일을 ec2 내부로 옮김
  4. ec2 내부에서 쉘 스크립트(deploy.sh, switch.sh)로 .jar 파일을 도커 컨테이너로 실행
  5. 추후 배포시 기존 동작하던 컨테이너가 아닌 다른 컨테이너로 새롭게 배포된 파일을 실행 시키고 기존에 돌아가던 도커 컨테이너 종료

1. nginx conf.d → service-url.inc 내부

set $service_url http://127.0.0.1:8081;
  • WAS가 돌아갈 포트 번호 지정

2. sites-enabled → default

# Your App
upstream yourapp {
    server localhost:5442;
}

upstream openviduserver {
    server localhost:5443;
}

server {
    listen 80;
    listen [::]:80;
    server_name pokerface-server.ddns.net;

    # Redirect to https
    location / {
        rewrite ^(.*) https://pokerface-server.ddns.net:443$1 permanent;
    }

    # letsencrypt
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;        #only allow requests from localhost
        deny all;               #deny all other hosts
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name pokerface-server.ddns.net;

    # SSL Config
    ssl_certificate         /etc/letsencrypt/live/pokerface-server.ddns.net/fullchain.pem;
    ssl_certificate_key     /etc/letsencrypt/live/pokerface-server.ddns.net/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/pokerface-server.ddns.net/fullchain.pem;

    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 5m;
    ssl_stapling on;
    ssl_stapling_verify on;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=63072000" always;

    # Proxy
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Proto https;
    proxy_headers_hash_bucket_size 512;
    proxy_redirect off;

		# Websockets
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    include /etc/nginx/conf.d/service-url.inc;

    # Your App
    location / {
        root /home/build;
        index index.html index.htm;
        try_files $uri $uri/ /index.html;

    }

    location /api {
        proxy_pass $service_url;
    }
  • “/” 주소로 들어오는 요청은 리엑트로 빌드한 정적 파일을 return
  • “/api” 주소로 들어오는 요청은 WAS로 연결

3. docker-compose.yml(nginx 부분만)

logging:
            options:
                max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"

    nginx:
        image: openvidu/openvidu-proxy:2.28.0
        restart: always
        network_mode: host
        volumes:
            - ./certificates:/etc/letsencrypt
            - ./owncert:/owncert
            - ./custom-nginx-vhosts:/etc/nginx/vhost.d/
            - ./custom-nginx-locations:/custom-nginx-locations
            - ${OPENVIDU_RECORDING_CUSTOM_LAYOUT}:/opt/openvidu/custom-layout
            - ./custom-nginx.conf:/custom-nginx/custom-nginx.conf
            - ./nginx.conf:/etc/nginx/nginx.conf
            - ./service-url.inc:/etc/nginx/conf.d/service-url.inc
            - ~/app/build:/home/build
        environment:
            - DOMAIN_OR_PUBLIC_IP=${DOMAIN_OR_PUBLIC_IP}
            - CERTIFICATE_TYPE=${CERTIFICATE_TYPE}
            - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
            - PROXY_HTTP_PORT=${HTTP_PORT:-}
            - PROXY_HTTPS_PORT=${HTTPS_PORT:-}
            - PROXY_HTTPS_PROTOCOLS=${HTTPS_PROTOCOLS:-}
            - PROXY_HTTPS_CIPHERS=${HTTPS_CIPHERS:-}
            - PROXY_HTTPS_HSTS=${HTTPS_HSTS:-}
            - ALLOWED_ACCESS_TO_DASHBOARD=${ALLOWED_ACCESS_TO_DASHBOARD:-}
            - ALLOWED_ACCESS_TO_RESTAPI=${ALLOWED_ACCESS_TO_RESTAPI:-}
            - PROXY_MODE=CE
            - WITH_APP=true
            - SUPPORT_DEPRECATED_API=${SUPPORT_DEPRECATED_API:-false}
            - REDIRECT_WWW=${REDIRECT_WWW:-false}
            - WORKER_CONNECTIONS=${WORKER_CONNECTIONS:-10240}
            - PUBLIC_IP=${PROXY_PUBLIC_IP:-auto-ipv4}
        logging:
            options:
                max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
                                                                                                                     124,20        Bot
  • Openvidu에서 제공하는 nignx의 설정을 custom 하기 위해 변경한 설정 파일을 도커 컴포즈로 마운팅하여 실행한다.
  • 추가 사항
    • ./custom-nginx.conf:/custom-nginx/custom-nginx.conf
    • ./nginx.conf:/etc/nginx/nginx.conf
    • ./service-url.inc:/etc/nginx/conf.d/service-url.inc
    • ~/app/build:/home/build

4. deploy.sh

#!/bin/bash
echo "> 현재 구동중인 profile 확인"
CURRENT_PROFILE=$(curl -s http://localhost/utils/profile)

sleep 5

echo "> $CURRENT_PROFILE"

if [ $CURRENT_PROFILE == set1 ]
then
  IDLE_PROFILE=set2
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
  IDLE_PROFILE=set1
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile: $CURRENT_PROFILE"
  echo "> default로 set1을 할당합니다. IDLE_PROFILE: set1"
  IDLE_PROFILE=set1
  IDLE_PORT=8081
fi

IMAGE_NAME=pokerface_server
TAG_ID=$(docker images | sort -r -k2 -h | grep "${IMAGE_NAME}" | awk 'BEGIN{tag = 1} NR==1{tag += $2} END{print tag}')

sleep 3

echo "> 도커 build 실행 : docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} ."
docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} /home/ubuntu/app

sleep 3

echo "> $IDLE_PROFILE 배포"
echo "> 도커 run 실행 :  sudo docker run --name $IDLE_PROFILE -d --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}"
docker run --name $IDLE_PROFILE -d --rm -p ${IDLE_PORT}:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}

sleep 3

sudo docker ps

echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s http://localhost:$IDLE_PORT/actuator/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then
    echo "> Health check 성공"
    break
  else
    echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
    echo "> Health check: ${response}"
  fi

  if [ $retry_count -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> Nginx에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done

echo "> 스위칭을 시도합니다..."
sleep 5

/home/ubuntu/app/switch.sh
  1. 현재 실행 중인 프로파일을 조회해서, 새로운 프로젝트가 배포될 IDLE_PROFILE을 지정

  2. 생성할 이미지의 이름 지정 및 tag 생성

    IMAGE_NAME=pokerface_server
    TAG_ID=$(docker images | sort -r -k2 -h | grep "${IMAGE_NAME}" | awk 'BEGIN{tag = 1} NR==1{tag += $2} END{print tag}')
    • IMAGE_NAME : 생성할 이미지 이름
    • TAG_ID : 이미지에 붙일 태그 지정
      • 뒤에 나오는 명령어들로 “이전에 생성한 이미지의 태그 번호 + 1”로 새로 생성할 이미지의 태그 번호를 지정한다.
    • docker images | sort -r -k2 -h | grep "${IMAGE_NAME}" : docker 이미지들을 생성일자의 내림차순(제일 최근 이미지가 가장 위에 뜸)으로 정렬 후 IMAGE_NAME 변수에 들어있는 문자열을 이미지 이름에 지닌 레코드를 가져온다.(like 정규표현식)
    • awk 'BEGIN{tag = 1} NR==1{tag += $2} END{print tag}' : awk 문법
      • begin & end : 명령의 시작과 끝에 수행
      • NR == 1{tag += $2} : NR이 특정 위치의 레코드 값을 들고 오는 역할로 여기선 첫번째 레코드를 들고 온후 tag에 2번 째 필드 값(이전 태그 숫자)을 가져와서 더해준다.
  3. 이미지 생성

    • docker build --build-arg IDLE_PROFILE=${IDLE_PROFILE} -t ${IMAGE_NAME}:${TAG_ID} /home/ubuntu/app
      • -arg 옵션으로 Dockerfile 내부에서 사용하는 변수들을 Dockerfile 밖에서 build 명령을 내리면서 초기화 할 수 있다.
      • build 명령 형식 : "-t <생성할 이미지명>:<태그명> <Dockerfile 위치>"
  4. 생성한 이미지를 컨테이너로 실행

    • docker run --name $IDLE_PROFILE -d -t --rm -p $IDLE_PORT:${IDLE_PORT} ${IMAGE_NAME}:${TAG_ID}
    • docker run [생성할 컨테이너 이름] 외부 포트:내부 포트 이미지이름:태그
  5. 새로 생성된 프로젝트에 대해 health check를 수행한다.

    #!/bin/bash
    echo "> 현재 구동중인 Port 확인"
    CURRENT_PROFILE=$(curl -s http://localhost/utils/profile)
    
    sudo docker ps
    
    if [ $CURRENT_PROFILE == set1 ]
    then
      CURRENT_PORT=8081
      IDLE_PORT=8082
    elif [ $CURRENT_PROFILE == set2 ]
    then
      CURRENT_PORT=8082
      IDLE_PORT=8081
    else
      echo "> 일치하는 Profile이 없습니다. Profile:$CURRENT_PROFILE"
      echo "> 8081을 할당합니다."
      IDLE_PORT=8081
    fi
    
    echo "> 현재 구동중인 Port: $CURRENT_PORT"
    echo "> 전환할 Port : $IDLE_PORT"
    echo "> Port 전환"
    echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
    
    sleep 3
    
    echo "> 기존에 실행 중이던 ${CURRENT_PROFILE} 컨테이너 삭제"
    sudo docker rm -f ${CURRENT_PROFILE}
    
    echo "> Nginx Reload"
    
    sudo service nginx reload
    sudo service nginx restart
    • health check 시 “UP”이 나오지 않는다면 비정상이라는 것이므로 UP이 나왔는 지 확인
    • 안나왔다면 10번 까지 다시 체크
    • 10번 후에도 나오지 않는다면 배포 종료
  6. switch.sh 스크립트를 실행하여 서버 스위칭(ex. 8081 -> 8082)

    echo "> 스위칭을 시도합니다..."
    sleep 5
     
    /home/ubuntu/app/switch.sh
    • switch.sh가 있는 파일 경로를 지정

5. switch.sh

#!/bin/bash
echo "> 현재 구동중인 Port 확인"
CURRENT_PROFILE=$(curl -s http://localhost/utils/profile)
 
if [ $CURRENT_PROFILE == set1 ]
then
  CURRENT_PORT=8081
  IDLE_PORT=8082
elif [ $CURRENT_PROFILE == set2 ]
then
  CURRENT_PORT=8082
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile:$CURRENT_PROFILE"
  echo "> 8081을 할당합니다."
  IDLE_PORT=8081
fi
 
echo "> 현재 구동중인 Port: $CURRENT_PORT"
echo "> 전환할 Port : $IDLE_PORT"
echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
 
echo "> ${CURRENT_PROFILE} 컨테이너 삭제"
sudo docker stop $CURRENT_PROFILE
sudo docker rm $CURRENT_PROFILE
 
echo "> Nginx Reload"
 
sudo service nginx reload
  1. 현재 가동 중인 서버(CURRENT_PROFILE), 새롭게 가동할 서버 포트(IDLE_PORT) 식별
  2. /etc/nginx/conf.d/service-url.inc 경로에 있는 nginx가 리버스 프록시 할 포트를 새롭게 가동할 서버의 포트번호로 수정
    1. tee : 표준 입력(standard input)에서 읽어서 표준 출력(standard output) 과 파일에 쓰는 명령어
  3. 현재 가동 중인 서버가 돌고 있는 컨테이너 정지 및 삭제
    1. nginx의 proxy_path를 수정했으므로 nginx 재시작

6. Dockerfile

FROM openjdk:11
ARG IDLE_PROFILE
ARG JAR_FILE=*.jar
ENV ENV_IDLE_PROFILE=$IDLE_PROFILE
COPY ${JAR_FILE} app.jar
RUN echo $ENV_IDLE_PROFILE
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${ENV_IDLE_PROFILE}", "/app.jar"]
  • FFOM : 이미지 생성 시 사용할 이미지
  • ARG : Dockerfile 내부에서 사용할 변수 지정
  • ENV : Dockerfile 내부에서 사용할 환경 변수
  • COPY : 호스트OS의 파일 또는 디렉토리를 컨테이너 안의 경로로 복사
    • *.jar(~~.jar)를 복사하여 컨테이너 내부에 app.jar를 생성
  • ENTRYPOINT : 컨테이너 시작 후 실행할 명령어를 지정
    • CMD 명령어와 유사하지만 ENTRYPOINT는 강제성을 지님(무조건 수행됨)
    • -Dspring.profiles.active=${ENV_IDLE_PROFILE} : 수행할 서버 타입(포트)를 지정

7. 젠킨스 성공 console output

Started by GitLab push by 서지오
Started by GitLab push by 서지오
Running as SYSTEM
Building in workspace /var/jenkins_home/workspace/pokerface
The recommended git tool is: NONE
using credential d36e63ee-e889-49ff-95d4-485fda318fcb
 > git rev-parse --resolve-git-dir /var/jenkins_home/workspace/pokerface/.git # timeout=10
Fetching changes from the remote Git repository
 > git config remote.origin.url https://lab.ssafy.com/s09-webmobile1-sub2/S09P12A603 # timeout=10
Fetching upstream changes from https://lab.ssafy.com/s09-webmobile1-sub2/S09P12A603
 > git --version # timeout=10
 > git --version # 'git version 2.30.2'
using GIT_ASKPASS to set credentials 
 > git fetch --tags --force --progress -- https://lab.ssafy.com/s09-webmobile1-sub2/S09P12A603 +refs/heads/*:refs/remotes/origin/* # timeout=10
skipping resolution of commit remotes/origin/feature/issue-116, since it originates from another repository
 > git rev-parse refs/remotes/origin/feature/issue-116^{commit} # timeout=10
Checking out Revision c120fa7c82368531110ff737d6c9e3b2eff57218 (refs/remotes/origin/feature/issue-116)
 > git config core.sparsecheckout # timeout=10
 > git checkout -f c120fa7c82368531110ff737d6c9e3b2eff57218 # timeout=10
Commit message: "Fix : Gitlab & Webhook & Jenkins & Nginx 무중단 배포 최종"
 > git rev-list --no-walk 996d0e0873673dcbc7db4caa8a3ce29b310b1ce5 # timeout=10
[Gradle] - Launching build.
[backend] $ /var/jenkins_home/tools/hudson.plugins.gradle.GradleInstallation/gradle-8.1.1/bin/gradle clean build
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :clean
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :bootJar
> Task :jar SKIPPED
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :build

BUILD SUCCESSFUL in 11s
7 actionable tasks: 7 executed
Build step 'Invoke Gradle script' changed build result to SUCCESS
SSH: Connecting from host [de6773f73db5]
SSH: Connecting with configuration [server-ec2] ...
SSH: EXEC: completed after 35,621 ms
SSH: Disconnecting configuration [server-ec2] ...
SSH: Transferred 1 file(s)
Finished: SUCCESS

8. 성공 Log

ubuntu@ip-172-26-1-239:~/app$ ./deploy.sh
> 현재 구동중인 profile 확인
> set1
> 도커 build 실행 : docker build --build-arg IDLE_PROFILE=set2 -t pokerface_server:2 .
[+] Building 0.7s (8/8) FINISHED                                                                                                                                                                                       docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                                                             0.0s
 => => transferring dockerfile: 266B                                                                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                                                                                                  0.0s
 => [internal] load metadata for docker.io/library/openjdk:11                                                                                                                                                                    0.6s
 => [internal] load build context                                                                                                                                                                                                0.0s
 => => transferring context: 52B                                                                                                                                                                                                 0.0s
 => [1/3] FROM docker.io/library/openjdk:11@sha256:99bac5bf83633e3c7399aed725c8415e7b569b54e03e4599e580fc9cdb7c21ab                                                                                                              0.0s
 => CACHED [2/3] COPY *.jar app.jar                                                                                                                                                                                              0.0s
 => CACHED [3/3] RUN echo set2                                                                                                                                                                                                   0.0s
 => exporting to image                                                                                                                                                                                                           0.0s
 => => exporting layers                                                                                                                                                                                                          0.0s
 => => writing image sha256:611a23c9bcf19235bcef4447b5880daabe3df5d2d0a0bc561ea83660d4b45789                                                                                                                                     0.0s
 => => naming to docker.io/library/pokerface_server:2                                                                                                                                                                            0.0s
> set2 배포
> 도커 run 실행 :  sudo docker run --name set2 -d --rm -p 8082:8082 pokerface_server:2
022affdf96e3fd148a39b8715b02a6bd1ffefdc5f6d0c2c53e96368a48516cbf
CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
022affdf96e3   pokerface_server:2    "java -jar -Dspring.…"   3 seconds ago    Up 3 seconds    0.0.0.0:8082->8082/tcp, :::8082->8082/tcp              set2
13ebae1049ec   pokerface_server:1    "java -jar -Dspring.…"   12 minutes ago   Up 12 minutes   0.0.0.0:8081->8081/tcp, :::8081->8081/tcp              set1
de6773f73db5   jenkins/jenkins:lts   "/usr/bin/tini -- /u…"   3 days ago       Up 2 days       0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 50000/tcp   jenkins
> set2 10초 후 Health check 시작
> curl -s http://localhost:8082/actuator/health
> Health check 성공
> 스위칭을 시도합니다...
> 현재 구동중인 Port 확인
CONTAINER ID   IMAGE                 COMMAND                  CREATED          STATUS          PORTS                                                  NAMES
022affdf96e3   pokerface_server:2    "java -jar -Dspring.…"   18 seconds ago   Up 18 seconds   0.0.0.0:8082->8082/tcp, :::8082->8082/tcp              set2
13ebae1049ec   pokerface_server:1    "java -jar -Dspring.…"   12 minutes ago   Up 12 minutes   0.0.0.0:8081->8081/tcp, :::8081->8081/tcp              set1
de6773f73db5   jenkins/jenkins:lts   "/usr/bin/tini -- /u…"   3 days ago       Up 2 days       0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 50000/tcp   jenkins
> 현재 구동중인 Port: 8081
> 전환할 Port : 8082
> Port 전환
set $service_url http://127.0.0.1:8082;
> 기존에 실행 중이던 set1 컨테이너 삭제
set1
> Nginx Reload

9. 성공한 결과 화면

ubuntu@ip-172-26-1-239:~$ docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                                                  NAMES
af0bec666875   pokerface_server:3    "java -jar -Dspring.…"   About a minute ago   Up About a minute   0.0.0.0:8081->8081/tcp, :::8081->8081/tcp              set1
de6773f73db5   jenkins/jenkins:lts   "/usr/bin/tini -- /u…"   3 days ago           Up 2 days           0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 50000/tcp   jenkins

# 이 사이에 깃 랩에 push event 발생

ubuntu@ip-172-26-1-239:~$ docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                                                  NAMES
500e0af2817b   pokerface_server:4    "java -jar -Dspring.…"   About a minute ago   Up About a minute   0.0.0.0:8082->8082/tcp, :::8082->8082/tcp              set2
de6773f73db5   jenkins/jenkins:lts   "/usr/bin/tini -- /u…"   3 days ago           Up 2 days           0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 50000/tcp   jenkins
ubuntu@ip-172-26-1-239:~$
  • Gitlab Webhook을 통해 Jenkins에서 event를 감지
  • Jenkins에서 Gradle을 사용해 내가 선택하는 브랜치를 빌드
  • Publish over ssh를 사용해 ec2로 빌드한 jar 파일 전송
  • EC2 내 deploy.sh, siwtch.sh, Dockerfile을 사용하여 ec2로 전송된 jar파일을 Docker 컨테이너로 실행
    • 이 과정 속 무중단 배포를 진행

참고 자료

profile
백엔드 개발자를 꿈꾸는 학생입니다!

0개의 댓글