도커랑 싸운 썰 풀어요 :: nginx 를 이용한 blue/green 방식 무중단 배포

정 승 연·2023년 5월 10일
0

땅굴파는 이야기

목록 보기
3/5
post-thumbnail

nginx 를 이용한 blue/green 방식 무중단 배포

S3 Deploy , jenkins , Travis 사용하지 않고 오직 Github actions를 사용해서 .

S3 Deploy , jenkins , Travis 를 사용하지 않고 Github Actions만 사용한 방식에 대한 레퍼런스가 없어, 제가 꼭 성공해서 그에 대한 기술 블로그를 작성하고 싶었는데요. 드디어 할 수 있게 되었습니다 하하

blue/green 방식에 대한 이해

blue/green 배포 방식에 대한 이해

먼저, blue/green 배포 방식에 대한 이해가 필요합니다.

blue/green 배포 방식은 트래픽을 한번에 구버전에서 신버전으로 옮기는 방식으로 blue와 green을 나란히 구성해 두 상태로 배포 시점에 트래픽을 blue에서 green으로 일제히 전환시킵니다.

현재 blue 컨테이너 8081포트를 바라보고 있지만

green 컨테이너가 활성화 되는 동안에도 요청은 blue 컨테이너로 reverse proxy 되기에 서비스는 중단되지 않습니다.

green 컨테이너가 활성화 되면, blue 컨테이너로 보내던 요청을 green으로 향하도록 바꾸고 nginx를 reload 시켜줍니다.

따라서 nginx는 green 컨테이너 를 바라보고 reverse proxy 시켜줍니다.

이를 통해 서버가 로드되는 시간을 nginx가 reload 되는 시간 만큼으로 줄일 수 있습니다.
-> 실제 실행시켜보니 reload 되는 약 30초 동안만 api 서버가 502 gateway 에러로 사용 불가능하고, reload 끝난 후 바로 사용 가능합니다

서버 리눅스 구조

source 디렉토리 안에는 /source/build/libs/BE-0.0.1-SNAPSHOT.jar

포트포워딩. 포트 연결

80으로 접속하면 → 8081에서 → 8080으로

aws-linux2의 로컬 nginx 설정.

nginx까지 도커에 올리고 싶었지만 그건 다음을 기약하며 .. EC2 서버의 내장 nginx 이용하여 프록시 진행

  1. nginx 설치

  2. /etc/nginx/sites-available/nginx.conf 수정하기. 없으면 만들기

    server {
            listen 80 default_server;
            listen [::]:80 default_server;
            root /var/www/html;
    
            server_name _;
    
            location / {
                try_files $uri $uri/ =404;
            }
    }

    sites-enabled에 symlink 만들기

    cd /etc/nginx/sites-enabled
    sudo ln -s /etc/nginx/sites-available/nginx.conf /etc/nginx/sites-enabled/
    sudo nginx -t
    sudo service nginx restart

    + nginx 완전 삭제

    nginx 를 설정하는 중에 잘못되었다고 생각하여 다시 처음부터 다시 설정하고 싶었다. 챗 지피티한테 물어본 결과 알려준 방법이다 우아우 체고 ..

    sudo systemctl stop nginx
    sudo apt-get purge nginx
    sudo apt-get purge nginx nginx-common
    sudo apt-get autoremove

docker-compose에 대한 이해

2개의 컨테이너 띄우기

docker와 docker-compose 의 차이

  • Docker는 single container를 관리하는 것

  • Docker-compose는 yml 파일 기반으로 multi container를 관리하는 것

  • docker-compose의 구성요소

    1. 각 컨테이너의 Dockerfile을 작성한다.

    2. docker-compose.yml을 작성하고 독립된 컨테이너의 실행을 정의한다.

    3. docker-compose up 으로 컨테이너를 개시한다.

      docker-compose up
      docker-compose build
      docker-compose up -it

      push 할 때 dockerhub에 같은 이름의 레포 있어야함.

blue/green 방식에 대한 이해

다음은 뒤에서 실행할 entry.sh 를 해석한 내용이다.

docker-compose ps를 통해 blue가 실행중인지 확인. 
실행중이 아니라면
	blue up, 
	before-compose-color=green, after-compose-color=blue
실행중이라면
	green up
	before-compose-color=blue, after-compose-color=green
새로운 컨테이너 띄운 후에 서버(aws-linux2) 의 nginx.conf 파일 수정

sudo nginx reload

이전 컨테이너 종료

docker-compose.blue.yml, docker-compose.green.yml 을 실행하는 entry.sh

위의 로직이 담긴 쉘 스크립트 명령어 파일을 actions.yml에서 실행.

그럼 blue/green으로 따로 빌드업을 하고? blue/green 방식에 따라 바꾸는 sh 작성

초반에, docker에서 nginx 설정까지 구성하는 레퍼런스를 참고하며 애를 많이 먹었다. nginx까지 도커에 올려 실행하면 좋지만, 굳이? (다음에 해보겠다) 라는 생각이 들었고 ec2 서버의 nginx를 이용해 프록시를 진행하려한다.

그렇기 위해서는

  1. 프록시 진행 시 해당 포트에 맞는 nginx.conf 설정을 변경해주어야함.
  2. blue/green 컨테이너에는 무엇이 들어있을까? 실행하는 포트번호만 담겨있는정도?

그래서 docker가 8081,8082 포트로 연결해주고,

ec2의 nginx가 실행중인 8081,8082 포트를 80으로 연결해주는것.

[ docker가 8081,8082 포트로 연결해주고, ] 이 과정은 docker-compose에서 진행하고,

[ nginx가 실행중인 8081,8082 포트를 80으로 연결해주는것. ] 은 /etc/nginx/nginx.conf 파일 변경을 통해 이루어 진다고 보면 된다.

이 일련의 과정은 모두 entry.sh에서 수행된다.

EC2에 sh 파일, 도커파일. docker-compose.yml, nginx.conf 파일 만들어두기

그래서 처음 시작 파일 구조에서 말한 바와 같이, /home/ec2-user에 다음 파일이 담겨있다.

# Dockerfile

FROM openjdk:11
ARG JAR_FILE=./source/build/libs/BE-0.0.1-SNAPSHOT.jar
ARG SPRING_CONFIG=./src/main/resources/application.yml
COPY ${JAR_FILE} app.jar
COPY ${SPRING_CONFIG} application.yml
ENTRYPOINT ["java","-jar","/app.jar","--spring.config.location=file:/application.yml"]
# docker-compose.blue.yml

version: '3'

services:

  blue:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8081:8080"
# docker-compose.green.yml

version: '3'

services:

  green:
      build:
        context: .
        dockerfile: Dockerfile
      ports:
        - "8082:8080"
# nginx.blue.conf

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {public IP}:8081; # blue

    }

     access_log /var/log/nginx/access.log;

    server {
	    listen 80;

        location / {
                  proxy_pass http://localhost:8081;
                  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;
        }

    }
}
# nginx.green.conf

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server {public IP}:8082; # green

    }

     access_log /var/log/nginx/access.log;

    server {
        listen 80;

        location / {
                  proxy_pass http://localhost:8082;
                  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;
        }

    }
}
# entry.sh

DOCKER_APP_NAME=meetup
# Blue 를 기준으로 현재 떠있는 컨테이너를 체크한다.

EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue-1 -f docker-compose.blue.yml ps | grep Up)

# 컨테이너 스위칭
if [ -z "$EXIST_BLUE" ]; then
    echo "blue up"
    docker-compose -p ${DOCKER_APP_NAME}-blue-1 -f docker-compose.blue.yml up -d
    BEFORE_COMPOSE_COLOR="green"
    AFTER_COMPOSE_COLOR="blue"
else
    echo "green up"
    docker-compose -p ${DOCKER_APP_NAME}-green-1 -f docker-compose.green.yml up -d
    BEFORE_COMPOSE_COLOR="blue"
    AFTER_COMPOSE_COLOR="green"
fi

sleep 10

# 새로운 컨테이너가 제대로 떴는지 확인
EXIST_AFTER=$(docker-compose -p ${DOCKER_APP_NAME}-${AFTER_COMPOSE_COLOR}-1 -f docker-compose.${AFTER_COMPOSE_COLOR}.yml ps | grep Up)
if [ -n "$EXIST_AFTER" ]; then

  # nginx.config를 컨테이너에 맞게 변경해주고 reload 한다
  cp ./nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/nginx.conf
	sudo  nginx -s reload

  # 이전 컨테이너 종료
  docker-compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR}-1 -f docker-compose.${BEFORE_COMPOSE_COLOR}.yml down
  echo "$BEFORE_COMPOSE_COLOR down"
fi

일단 먼저 로컬에서 실행을 해봤다. ./entry.sh 를 실행하면 다음과 같다. 우아우

로컬에서 실행하려면 똑같이 프로젝트 파일 최상위에 Dockerfile, docker-compose.blue.yml .. 등 있어야한다.

Gihub Actions에서 scp로 ec2에 build 파일 보내기 && entry.sh 실행

이제 github actions 에서 jar 파일 build, ec2로 파일 전송, entry.sh 파일 자동 실행 되도록한다.

로컬에서 실행할 때는 ./entry.sh 했는데, github actions에선 sh /entry.sh 해야한다.

# actions.yml

name: Deploy

on:
  push:
    branches:
      - feat/docker

jobs:
  build:                    
    runs-on: ubuntu-latest   
    env :
      working-directory: ./
      APPLICATION: ${{ secrets.APPLICATION }}
      
    steps:                   
    
    - uses: actions/checkout@v2
    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'adopt'
        
    - name: Cache Gradle packages
      uses: actions/cache@v2
      with:
        path: |
             ~/.gradle/caches
             ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
              ${{ runner.os }}-gradle-
              
    - uses: actions/checkout@v2
    - run: |
          mkdir ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml 
          echo "${{env.APPLICATION}}" > ./application.yml
          
    - uses: actions/upload-artifact@v2
      with:
        name: application.yml
        path: ./src/main/resources/application.yml
          
          
    - name: gradlew 실행권한 주기
      run: chmod +x gradlew

    - name: 스프링부트 애플리케이션 빌드 
      run: ./gradlew clean build

    - name: Login to DockerHub
      run:
         docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}

    - name: **entry.sh push to ec2**
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.EC2_SERVER_HOST }}
        username: ec2-user
        key: ${{ secrets.PRIVATE_KEY }}
        source: "build/libs/*.jar"
        target: "source"
        rm: true

    - name: **entry.sh**
      uses: appleboy/ssh-action@master
      with:
          host: ${{ secrets.EC2_SERVER_HOST }}
          username: ec2-user
          key: ${{ secrets.PRIVATE_KEY }}
          script: |
              ls -al
              chmod 777 entry.sh
              sh /home/ec2-user/entry.sh

방화벽 부시기

다 했고, 다 했는데. 80포트로 접근이 안됐다.

포트도 활짝 열어두었고, nginx 설정도 모두 다 해두었는데, 자꾸 연결확인, 방화벽 확인 에러가 떴다.

근데 심지어

EC2 터미널에서 curl localhost 해도 됨. curl localhost/test 해도 됐다.

Postman 에서 {public IP} GET 접속하거나 그냥 크롬으로 접근하면 안됐다. 왜 안될까 ..

여러가지 방법을 시도해보았다.

  1. nginx.conf 파일중 upstream 서버 부분을 원래

    server localhost:8082; 이렇게 해뒀었다. 이게 문제였을까? 일단 퍼블릭 IP로 변경.

    upstream backend {
            server {public IP}:8082; # green
    }
  2. 방화벽을 부셔버렸다

    지난번에도 같은 오류를 겪은 적이 있었다. 왜 내 컴퓨터는 매번 방화벽이 문제일까.

    80 포트가 열려있는지 확인하는 명령어로 포트의 상태를 확인한다.

    telnet localhost 80

    닫혀있었다. 그래서 이번에도 방화벽 문제가 아닐까? 하고 방화벽을 부셔보았다. 생각보다 간단하다.

    systemctl stop firewalld 
    systemctl disable firewalld

    방화벽을 부시고 나니 80포트에서도 접근 가능했다. 그렇지만 이는 너무 위험하기에, 다시 방화벽을 시작하고 80포트의 방화벽만 풀어주기로한다.

    systemctl start firewalld 
    firewall-cmd --state 
    firewall-cmd --add-port=80/tcp 

    그럼 이제 진짜 80포트에서 접근 가능하다.
    nginx config 변경되는 약 30초 동안만 nginx 502 gateway 에러가 나고 nginx config 변경 직후 바로 배포되어 사용 가능하다. 최고!

0개의 댓글