blue green 배포 방식

실제로 우리팀에서 사용했던 무중단 배포 방식이다.

Spring을 예로 들면 전체적인 흐름은 jenkins를 통해 빌드 후, 빌드 된 jar파일을 배포를 관리하는 폴더로 복사한다. sh 스크립트를 통해 관리하기 되는데 docker compose를 통해 새로운 버전의 컨테이너를 띄우고 이전 버전을 내린다.. 말로는 참 쉽다. ;-; 

블루 그린 배포 - 개념 

블루 그린 배포는 새로운 버전의 인스턴스를 띄운 뒤, 로드밸런서를 통해 새로운 버전의 인스턴스로 트래픽을 요청한다. 

 

블루 그린 배포를 이용하면 기존에 인스턴스를 유지하던 비용의 2배가 들어가는 시점이 존재한다, 또한 새로운 버전에 치명적인 오류가 있다면 전환된 서버는 모두 내려갈 것이다. 하지만 구버전의 인스턴스가 그대로 남아있기 때문에 롤백이 손쉽고, 차마 테스트하지 못한 부분에서 치명적인 오류가 발생했다면 구버전으로 다시 트래픽을 돌리기 간편하다. 

 

블루 그린 배포 - 실습 

1. jenkins pipe line 

		# 스토리 관련 스테이지
	stage('Building story image'){
            steps{
                script {
                  dockerImage = docker.build("jodong2/story-zero-downtime", "/var/jenkins_home/workspace/deploy_test/StoryModule")
                  withCredentials([usernamePassword(credentialsId: 'jodong2', usernameVariable: 'DOCKER_HUB_USERNAME', passwordVariable: 'DOCKER_HUB_PASSWORD')]) {
                     sh 'docker login -u $DOCKER_HUB_USERNAME -p $DOCKER_HUB_PASSWORD'
                     sh 'docker push jodong2/story-zero-downtime'
                  }
                }
            }
        }
       stage('Run docker story image') {
          steps {
              dir('/var/jenkins_home/workspace/deploy/StoryModule'){
                  sh 'chmod +x deploy.sh'
                  sh './deploy.sh'
               }
            }
        }

이미지를 빌드하고 deploy.sh를 실행한다. 

#!/bin/bash

#story deploy

export DOCKER_REGISTRY=jodong2 DOCKER_APP_NAME=story-zero-downtime IMAGE_TAG=latest

EXIST_BLUE=$(docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml ps | grep Up)
# 블루가 떠있는지 확인 

if [ -z "$EXIST_BLUE" ]; then
    echo "blueis is not exist. so make blue container"
    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"
    echo "end"
else
    echo "blue is exist. so make green container"
    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 10

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

    docker compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
    echo "$BEFORE_COMPOSE_COLOR down"
fi

deploy.sh파일은 떠있는 blue, green에 따라 각 docker-compose.yaml파일을 실행하게 된다. 

컨테이너가 띄워지는 시간으로 sleep 10초를 걸어놨는데, spring 메인 서버 같은 경우엔 애플리케이션을 시작하는데 약 20초를 필요로 한다. (사실 무중단임에도 팀원이 잠깐 내려가는거 같은데? 하길래 부랴부랴 수정했다..) 따라서 해당 메인 서버의 sh파일은 sleep이 25초로 설정했다. 

 

이후 다른 색의 컨테이너가 성공적으로 띄워졌다면 이전 컨테이너를 중지한다.

version: '3.7'

services:

  api:
    image: ${DOCKER_REGISTRY}/${DOCKER_APP_NAME}:${IMAGE_TAG}

    container_name: ${DOCKER_APP_NAME}-blue

    environment:
      - LANG=ko_KR.UTF-8

    ports:
      - '{port1}:8080'



    volumes:
      - story-logs:/var/log/app

volumes:
  story-logs:

port의 경우 blue와 green을 다르게 두고 해당 포트를 기준으로 로드밸런서가 구분할 수 있도록 하였다. 

예를들어 blue는 8081, green은 8082를 열어둔다. aws에서 보안설정도 잘 확인하자.... ;-; 

# 문제가 좀 있는.. upstream 구성.. 
upstream story {
    least_conn;
    server {ip}:{blue port1};
    server {ip}:{green port2};
    server {ip}:{blue port1};
    server {ip}:{green port2};
}

Nginx에서 upstream을 통해 blue green 각 서버로 연결한다. 

 이외에도 헬스체크 기능을 활용해서 죽은 서버에는 요청을 할 수 있지만 연결을 실패할 경우 nginx는 정해진 알고리즘에따라 다음 서버로 요청을 보낸다. 따라서 별도의 설정 없이도 올라가있는 컨테이너에 요청을 보낼 수 있다. 가장 처음 시도한 방식이고,,, 죽은 서버에 요청을 보내고 실패를 돌려받은 뒤 다음 서버에 요청을 보내는건 문제가 있는 방식이다.

 

Nginx - Health Check 

죽은 서버로 요청을 보내지 않기 위해 health check하는 방법이 있다. 서버가 정상 상태인지 실시간으로 계속 확인하는 방법으로 정상서버의 서버에게만 트래픽을 분배한다. 

upstream story {
    least_conn;
    server {ip}:{blue port1};
    server {ip}:{blue port1};
    check interval=3000 rise=2 fall=5 timeout=4000 type=http;
    check_http_send "HEAD / HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

추가된 내용을 보면 3초단위로 2번 성공하면 정상, 5번 실패하면 비정상으로 판단한다. health check는 http프로토콜을 사용하고 HTTP1.0을 사용하여 보낸다. HTTP1.1~2를 주로 사용하지만 1.0을 사용한 이유는 연결에 성공해도 지속하지 않고 바로 끊는 특징을 갖고 있어서 1.0을 사용했다. 또한 응답코드가 2xx, 3xx라면 정상상태로 판단한다.

죽은서버에 요청을 보내며 네트워크에 지연이 발생할 수 있었던 설정을 위와 같이 죽은 서버에는 요청을 보내지않도록하는 Nginx의 health check기능을 사용하는 방법이 있었다.

 

 

2개의 nginx 설정파일과 deploy.sh 변경

docker compose로 컨테이너를 삭삭(팀원말투인데 머리속에 떠오르 짧고 간결한 표현이 이거밖에 안떠오른다ㅂㄷㅂㄷ; ) 바꾸는데 Nginx도 같이 갈아끼우고 reload하면 되지 않을까? reload하는 순간 짧은 down time이 있지만 health check한다고 자원을 계속 소모하는 방법보단 짧은 down time으로 자원을 아낄 수도 있다. 

 

변경된 deploy.sh이다.

#!/bin/bash

#story deploy

export DOCKER_REGISTRY=jodong2 DOCKER_APP_NAME=story-zero-downtime IMAGE_TAG=latest

EXIST_BLUE=$(docker compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yaml ps | grep Up)
# 블루가 떠있는지 확인 

if [ -z "$EXIST_BLUE" ]; then
    echo "blueis is not exist. so make blue container"
    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"
    echo "end"
else
    echo "blue is exist. so make green container"
    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 10

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
	sudo ln -sf /etc/nginx/sites-available/nginx.${AFTER_COMPOSE_COLOR}.conf /etc/nginx/sites-enabled/nginx.conf
        sudo service nginx reload
    docker compose -p ${DOCKER_APP_NAME}-${BEFORE_COMPOSE_COLOR} -f docker-compose.${BEFORE_COMPOSE_COLOR}.yaml down
    echo "$BEFORE_COMPOSE_COLOR down"
fi

다음 색깔의 컨테이너가 제대로 떴다면 다음색깔에 맞는 nginx 설정파일로 변경한뒤 reload 한다. 

reload하는 과정에서 짧은 down time이 발생할 수 있어서 무중단배포라기엔 칭호가 조금 아쉽지만 처음 생각한 방식은 이렇다.. 

 

nginx의 설정파일은 

upstream story {
    least_conn;
    server {ip}:{port1};
}

이렇게 간결해졌고 그에맞는 ip:port만 다른 파일을 설정해뒀다. 

server가 한대라면 location에서 바로 쏴도 될 듯 싶다 ! 

 

 


 

저번주에 올렸어야됐는데.. 할 게 많았다. 

프로젝트하랴 CS공부하랴..싸피 마지막 플젝이 끝났다 ! 

끝나도 시키는게 뭔가 많다. 서버 관련 설정파일은 나중에 다시 보기위해 저장해놔야되는데.. 아이고. .언제다하지.. 

다음주 주말에는 별일이 없다면 자바나 파이썬에 관련된 글을 올릴듯 싶다. 

 왜냐면.. 저번주에 자바에 SOLID에 대해 공부했었다. 옛날옛적에 면접준비한다고 부랴부랴 공부했을때는 이해가 잘 안됐는데 이번엔 "아이고 내 코드는 객체지향을 개떡같이 사용하고 있구나!" 를 느낄 수 있었다. 그래서 다음에 진행하게 될 프로젝트나 기회가 된다면, 지금까지 해왔던 프로젝트 하나를 잡고 수정해볼 생각이다. 

 파이썬을 포스팅한다면... 파이썬의 GIL에 대해 시간을 길게 잡고 자세하게 포스팅해볼 생각이다..

 

 

 

발그림ㅎㅎ;

롤링 배포 - 개념

 롤링 배포는 현재 사용 중인 인스턴스 내에서 새 버전으로 하나씩 교체해나가는 것이다.

 롤링 배포를 이용하게 된다면 기존에 유지하고 있는 인스턴스로만 진행하기 때문에 추가적으로 비용이 발생하지않는다. 또한 점진적으로 업데이트하기때문에 롤백이 비교적 쉽다. 하지만 새로운 버전으로 교체하는 과정에서 일부 인스턴스가 연결되어 있지 않기 때문에 트래픽을 감당할 수 있는 선에서 진행해야하며 이전 버전과 새로운 버전의 호환성 문제도 생각해야 한다. 

 

롤링 배포 - 실습

 

1. spirng actuator 설정

build.gradle

다음과 같이 build.gradle에 spring actuator 의존성을 추가한다. 

spring actuator는 서버의 여러가지 상태를 확인할 수 있는 기능이다. 하지만 민감함 정보도 포함될 수 있으므로 우리가 필요한 health만 사용하도록 yml(properties)파일을 수정해주자. 

application.properties

설정 후 /actuator/health 를 확인하면 {"start":"UP"} 을 확인할 수 있다.

 

2. nginx 설정

nginx pro버전에서는 health-check 기능을 지원하지만, 내 지갑은 휑하므로 무료 health-check 모듈이 포함된 nginx를 사용할 것이다. 

먼저 nginx 설정 파일을 만들어보자 

vi nginx.conf

events {}

http {
  upstream market {
	# 배포 중인 서버들
	server ip:port;
	server ip:port;

	#health-check
	# interval - 3초씩에 한 번씩 확인
	# rise - 2번 이상 응답에 성공하면 서버가 살은 것으로 판단
	# fall - 5번 이상 응답에 실패할 경우 서버가 죽은 것으로 판단
	# timeout - 응답 시간 초과 1초
	check interval =3000 rise=2 fall=5 timeout=1000 type=http;
	# GET /actuator/health으로 healthcheck 요청
	check_http_send "GET /actuator/health HTTP/1.0\r\n\r\n";
	check_http_expect_alive http_2xx http_3xx;
  }

  server {
    listen 80;
    location / {
       proxy_pass http://market;
    }
    # 서버 상태 확인 url
     location /status {
     check_status;
   }
  }
}

다음 nginx image를 빌드하기 위해 Dockerfile을 작성한다. 

vi Dockerfile

# mrlioncub/nginx_upstream_check_module - health-check 모듈이 포함된 nginx image
FROM mrlioncub/nginx_upstream_check_module
COPY nginx.conf /etc/nginx/nginx.conf

Docker file을 작성했으니 이미지를 빌드하고 실행하자 

docker build --tag nginx:test-nginx .
docker run -d --name webserver -p 80:80 [tag | image id]

 

3. Shell script 작성

  • jenkins에서 배포 후에 실행할 shell script를 서버에 작성해두자
  • rollingdeploy.sh의 전체적인 흐름은 다음과 같다.
    • 현재 배포하려는 서버 외에 트래픽을 받을 수 있는 서버, health-check 없다면 배포하지 않음
    • 현재 tomcat server 종료 gracefully shut down -> force shut down
    • 배포 시작
    • 배포 후 자가 health-check

 

더보기
BASE_PATH=/home/app/
JAR_NAME=xxx.jar
echo "> build 파일명: $JAR_NAME"

#배포중인 server ip
IP1=# 
IP2=# 
#배포 port
DEPLOYED_PORT=8080


#private ip
MY_IP=$(hostname -i)

loop=1
limitLoop=30
flag='false'


if [ $MY_IP == $IP1 ]; then
  OTHER_IP=$IP2
elif [ $MY_IP == $IP2 ]; then
  OTHER_IP=$IP1
else
  echo "> 일치하는 IP가 없습니다. "
fi

#==========================살아있는 서버가 존재하는지 확인==============================
echo "> 서버 체크 시작"
for retry_count in {1..10};
do
  response=$(sudo curl -s http://$OTHER_IP:$DEPLOYED_PORT/actuator/health)
  up_count=$(echo $response | grep 'UP' | wc -l)
  echo "> $retry_count : $response  : $up_count"
  if [ $up_count -ge 1 ]; then
    echo "> 서버 health 체크 성공"
    break
  fi
  if [ $retry_count -eq 10 ]; then
    echo "> 서버 health 체크 실패"
    exit 1
  fi
  echo "> 실패 10초후 재시도"
  sleep 10
done

#===================================프로세스 종료======================================
# tomcat gracefully shutdown
echo "> 구동중인 애플리케이션 pid 확인"
IDLE_PID=(`ps -ef | grep  $JAR_NAME | grep -v 'grep' | awk '{ print $2 }'`)
 if [ ${#IDLE_PID[@]} = 0 ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
  flag='true'
else
  for pid in "${IDLE_PID[@]}"
  do
      echo "> [$pid] gracefully shutdown"
      kill -15 $pid
  done
  while [ $loop -le $limitLoop ]
  do
      PID_LIST=(`ps -ef | grep  $JAR_NAME | grep -v 'grep' | awk '{ print $2 }'`)
      if [ ${#PID_LIST[@]} = 0 ]
      then
          echo "> gracefully shutdown success "
          flag='true'
          break
      else
          for pid in "${PID_LIST[@]}"
          do
              echo "> [$loop/$limitLoop] $pid 프로세스 종료를 기다리는중입니다."
          done
          loop=$(( $loop + 1 ))
          sleep 1
          continue
      fi
  done
fi
if [ $flag == 'false' ];
then
    echo "> 프로세스 강제종료 시도"
    sudo ps -ef | grep $JAR_NAME | grep -v 'grep' |  awk '{ print $2 }' | \
    while read PID
    do
        echo "> [$PID] forced shutdown"
        kill -9 $PID
    done
fi

#===================================배포======================================

echo "> 배포"
echo "> 파일명" $BASE_PATH$JAR_NAME
sudo nohup java -jar -Dspring.profiles.active=prod $BASE_PATH$JAR_NAME & 
sudo sleep 10

echo "> 10초 후 Health check 시작"
echo "> curl -s http://$MY_IP:$DEPLOYED_PORT/actuator/health"

#==========================현재 서버 Health check============================
for retry_count in {1..10}; do
  response=$(sudo curl -s http://$MY_IP:$DEPLOYED_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 연결 실패. 재시도..."
  sudo sleep 10
done

sleep 60 # 다음 배포 서버를 위한 지연

 

4. Jenkins에서 배포 시 deploy.sh 실행.

 

 

무중단 배포 - 등장

 나는 귀여운 주니어 개발자로 옛날과 비교하는게 좀 웃기지만, 옛날에 비해 요즘의 서비스는 릴리즈 주기가 몹시 짧다. 

 

 대부분의 SW 개발 방법론이 폭포수 방식에서 애자일 방식으로 바뀌며 릴리즈 주기가 긴 소프트웨어는 적어졌다. 그만큼 제품은 더 빨리 출시되고, 기능 추가 등의 업데이트 등을 위해 배포가 되는 주기가 짧아졌다. 

 

 또한 대부분의 큰 소프트웨어에서는 하나의 거대한 아키텍처로 서비스하는 모놀리식 아키텍처에서 서비스를 모듈화하여 개발하고, 모듈별로 배포할 수 있는 마이크로 서비스 아키텍처로 바뀌며 배포하는 크기와 빈도가 달라졌다. 

 

 폭포수에서 애자일, 모놀리식에서 마이크로서비스 등의 변화를 통해 얻은 이점은 보다 잦은 배포를 통해 시장과 사용자의 요구를 빠르게 충족시키고 서비스의 가치를 높이기 위함이다. 하지만 이런 변화는 운영 안정성 측면에서 부정적인 영향을 미칠 수 있다.

 

 예를 들어 개발 결과물을 제공하기위해 서버에 배포한다고 가정하자. 최신 어플리케이션은 클라우드 기반으로 구성되어 트래픽에 따라 탄력적인 확장과 고가용성을 보장하지만, 배포 시에는 서비스를 멈춰야 하는 중단 배포 방식의 경우 다운타임이 발생한다. 물론 다운타임이 계획적으로 허용될 수 있다. 은행과 같은 서비스에 유연함에 제한이 있는 서비스가 그렇다. 하지만 전 세계의 사용자를 대상으로 하거나, 24시간 운영이 필요한 서비스라면 항상 가동되어야하는게 좋다. 일반적으로 중단 배포 방식은 사용자에게 불편함과 손실을 야기할 수 있다. 새로운 배포로 인해 문제가 발생하는 경우엔.. 야근해야지 뭐...

 

 야근이 좋은게 아니라면, 혹은 팀원에게 형 지금 배포중인거 맞지? 소리 듣기 싫은 개발자들은 무중단 배포 방식을 공부하자. 안정적인 배포 체계를 갖추는 것은 다양한 요구사항에도 개발에 집중할 수 있으며 이는 곧 경쟁력이 되고 내 수면시간이 된다. ㅎㅎ;

 

무중단 배포 - 개념

 무중단 배포는 서비스 장애와 배포의 부담을 최소화 하기 위해 운영중인 서비스를 중단하지 않고, 신규 소프트웨어를 배포하는 기술이다. 

 핵심은 *로드밸런서(Load Balancer)를 통해 연결된 두 개 이상의 (서로 다른 IP 혹은 포트를 가진)인스턴스에 트래픽을 제어해 배포하는 것이다. 배포 작업이 서비스에 영향을 주지 않도록 하기 위해 사용자의 이용량에 따라 인스턴스는 물론, 로드밸런서도 다중화를 고려해야한다. 

 

 다양한 무중단 배포 방식이 있는데 대표적으로 3개의 방식을 앞으로 3번에 걸쳐 포스팅하려한다.

  •  제한된 자원에서 하나씩 배포하여 변경해 나가는 롤링 배포
  •  현재 사용중인 버전의 인스턴스 수만큼 새 버전의 인스턴스를 준비해 로드밸런서가 스위칭해주는 블루-그린 배포,
  • 새로운 버전 소프트웨어의 모니터링과 검증에 초점을 맞춘 카나리 배포

로드밸런서가 오래된 버전의 서버와 새로운 버전의 서버를 구분하여 요청을 처리하는 것이 가장 큰 동작개념이다.

 

 

롤링, 블루-그린, 카나리 맛보기.

더보기
출처 : 구글에 업데이트 중 검색결과

 윈도우 업데이트를 기다리는게 싫다.

(컴퓨터는 2대를 사용중이다.)

롤링 배포 : 1번 컴퓨터 업데이트하는 중엔 2번 컴퓨터를 쓰고, 1번 컴퓨터 업데이트가 끝나면 2번 컴퓨터를 업데이트하며 1번 컴퓨터를 사용

블루 그린 배포 : 누나 컴퓨터 2대에 업데이트 진행하면서 내꺼 쓰고 누나 컴퓨터에 업데이트 끝나면, 내꺼 2대랑 바꾸기.

카나리 배포 : 1번 컴퓨터 업데이트하고 잘되나 안되나 확인 후에 2번 컴퓨터도 업데이트.

 


 

 

이미지 출처 : https://tecoble.techcourse.co.kr/post/2021-11-07-load-balancing/

로드밸런싱

 로드밸런싱은 서버가 처리해야 할 요청(Load)을 여러대의 서버로 나누어(Balancing) 처리하는 것을 의미한다. 한 대의 서버로 부하가 집중되지 않도록 트래픽을 관리해 각각의 서버가 최적의 퍼포먼스를 보일 수 있도록 하는 것이 목적이다. 

 

  서비스 초기, 하나의 서버로 모든 클라이언트의 요청을 처리할 수 있다면 로드밸런싱은 필요없다. 하지만 사용자가 많아져 현재의 서버에서 모든 요청을 처리할 수 없다면 *scale-up과 scale-out을 고려해야하고, scale-out을 선택한다면 로드밸런서는 필수적이다. 

 

 

 

scale-up VS scale-out

내가 두 배 똑똑해지기 VS 내가 두 명

 scale-up의 경우 서버자체의 성능을 향상시키는 것을 뜻한다. 예를들어 CPU가 i3인 컴퓨터에서 i7으로, m1에서 m2로 업그레이드하는 것과 같다. 주로 하나의 서버에서 모든 데이터가 관리되는 데이터베이스 서버(==RDB)에 적합한 방식이다. 데이터 정합성이나, 이슈관리가 쉽지만 성능향상에 한계가 존재하고, 서버 교체나 업그레이시 다운타임이 발생하며 서버 한대가 부담하는 요청이 많으므로 문제가 생기면 더 큰 타격을 입게 된다. 

 

 scale-out의 경우 서버를 증설하는 것을 뜻한다. 기존 서버와 동일하거나 낮은 성능의 서버를 추가로 운영하는 것으로, i3인 컴퓨터 추가로 구매하는 것과 같다. 서버 증설은 서버 성능 향상보다 비교적 비용 부담이 적으며, 분산처리에 적합하다. 일반적으로 모든 서버가 동일한 데이터를 갖고 있어야 하므로, 데이터의 변화가 빈번하지 않은 웹 서버에 적합한 방식이다. 

 

 

 

 

현재 ec2 상황은 아래의 모든게 만족됐다고 가정하자.

더보기

 

목표는 다음과 같다. 

ㅋㅋ발그림ㅋㅋㅋ엌ㅋㅋㅋㅋ..

젠킨스는 나중에 따로 정리하겠다. (여기서 칠 명령어를 젠킨스에 입력해놓으면 해주니까..)

 

backend나 frontend나 큰 흐름은 아래와 같다.

(window나 맥 등 로컬에 도커데스크탑을 설치하자... 어렵지 않으니 생략. )

1. 로컬에서 도커 이미지 빌드

2. 로컬에서 도커 이미지를 도커 허브에 푸시

3. ec2에서 도커이미지를 풀

4. ec2에서 도커이미지 실행

 

frontend를 도커를 통해 ec2에 띄워보자

위의 그림을 보듯 frontend를 띄운 도커는 nginx가 필요하다. nginx를 base image로 받아오고, 받아온 nginx의 기존 설정을 내가 원하는대로 바꿔야한다. nginx 설정파일부터 만들어보자. 

어디다 만들든 크게 상관은 없지만 밑에 작성한 Dockerfile을 그대로 복사해서 사용할거라면 프로젝트의 프론트엔드 폴더 최상단에 만들자.

1. 로컬에서 프로젝트 프론트엔드 폴더의 최상단에 default.conf 작성 (nginx 설정파일)

#default.conf
server{
    listen 80;
    location / { 
        root /app/build;
	index index.html index.htm;
        try_files $uri $uri/ /index.html;
    }
}



2. 로컬에서 프로젝트 프론트엔드 폴더의 최상단에 Dockerfile을 작성한다. 

FROM nginx #base이미지로 nginx를 받아온다.
RUN mkdir /app #컨테이너 내부에 app이라는 폴더를 만든다.
WORKDIR /app #워크디렉토리를 설정한다.
RUN mkdir ./build  #컨테이너 내부에 build라는 폴더를 생성한다.
ADD ./build ./build #프로젝트 폴더에 있는 build라는 폴더를 컨테이너 내부에 생성한 build 폴더에 복사한다.
RUN rm /etc/nginx/conf.d/default.conf #받아온 nginx 내부에 기존 설정값을 제거한다. 
COPY ./default.conf /etc/nginx/conf.d #만들어둔 nginx 설정값을 받아온 nginx의 설정값으로 대체한다.
EXPOSE 80 #80포트 사용
CMD ["nginx","-g", "daemon off;"] #nginx 실행

3. npm run build를 통해 css, html등 정적인 파일을 생성한다. 

프론트엔드 폴더 상단에서 

npm run build

4. docker image 빌드하기 

docker build --tag frontend .

뒤에 . 은 현재 폴더의 dockerfile을 빌드하겠다는 뜻으로 빌드하려는 dockerfile의 경로를 입력하자.

tag는 생성되는 이미지의 이름과 태그를 설정할것이다. 

이름만 설정할 경우 자동으로 tag는 latest로 설정된다.

frontend 라는 이미지가 빌드 된걸 확인해보려면

$ docker images

명령어로 확인할 수 있다.

 

5. docker 이미지 푸시하기 

docker tag [ image name or Tag ] [ docker hub ID 혹은 private registry ip:port ]/[ push image이름 ]

ex) docker tag frontend jodong2/frontend

를 통해 태그를 달고, 아래와 같은 명령어로 docker hub에 푸시하자.

docker push [이미지 이름:태그]

ex) docker push jodong2/frontend

 

6. ec2에서 docker image pull 받기

이전에 mysql을 설치했던 과정과 똑같다.

ec2의 ubuntu환경에서 다음 명령어를 실행하자.

$ docker pull [ docker hub ID 혹은 private registry ip:port ]/[ push image이름 ]

ex) docker pull jodong2/frontend

 

7.ec2에서 docker image 실행하기 

위의 목표 예시 발 그림을 보면 frontend는 host 의 xxxx port를 사용할 것이다. 

frontend의 nginx 설정 파일을 보면 80 포트를 받고 있다.

그러므로 -p옵션을 통해 xxxx port와 컨테이너의 80포트를 연결해야한다. 

$ docker run -d --name {container name} -p xxxx:80 [ docker hub ID 혹은 private registry ip:port ]/[ push image이름 ]

ex) docker run -d --name front -p xxxx:80 jodong2/front

 

8. ec2에서 도커 컨테이너 확인하기 

docker ps

 

 

 

Backend는 다음 포스팅에 :)

 


 

 

왜 Front는 nginx가 하나 더 필요할까?

spring의 경우 was로 tomcat이 있기 때문에 상관없지만 프론트의 경우 build된 정적파일을 처리할 수 서버가 따로 없다. ec2에 nginx가 있지만 도커를 활용해 띄운다면 내부에 별도의 서버가 필요하고 이때 proxy server로 nginx를 활용한 것이다.

 

 

왜 도커를 사용하여 띄울까?

docker 안쓰고 서버에다가 버전에 맞는 jdk설치하고 jar 파일 가져와서 실행하면 되는거 아니야?

된다. 불편할 뿐이다. 많고 많은 불편한 이유가 있겠지만 나에게 와닿은 예시는 수평확장에 대한 예시였다. 

꼭 수평확장 문제뿐만이 아니니 공부한다는 생각으로라도 도커를 사용해보자.

 

이전에 부스트캠프 ai tech에서 했던 프로젝트 중에 팀원들 모두 합쳐서 6대의 서버가 팀에 부여됐다. 

모두 동일한 작업을 처음부터 진행해가며 환경이 얼추 비슷했고 마지막엔 거의 비슷한 환경이 구성됐다. 근데 만약 도커를 사용하지 않았고, 하나의 서버에 openCV 설치가 제대로 되지 않았으며, 해당 library를 사용하는 함수를 호출한다면, 그 서버는 에러를 보여줄 것이다. 16.6%확률로 실패하는 기능이 있다면 가용성 측면에서 말도 안되는 서비스고 이걸 테스트하기 위해서 6대의 서버를 전부 테스트해야한다. 불편할게 확실하니 이거 확인하는 툴도 이미 만들어져있겠지만.... 7번째 서버가 추가되면 모든 환경을 처음부터 다시 구성해야한다. 

하지만 도커는 같은 환경을 보장한다. 7번째 8번째 서버가 추가된다해도, 손쉽게 똑같은 환경에서 서비스를 동작시킬 수 있다. 

 

금속활자로 책을 순식간에 동일한 컨디션의 동일한 모양으로 찍어낼 수 있는데 한 글자씩 붓으로 적고 있진 않은가 생각해보자 ㅠ

 

 

아래 블로그에 정리가 너무 잘돼있다. 저장해놓고 정독해야지.

https://www.44bits.io/ko/post/why-should-i-use-docker-container

 

왜 굳이 도커(컨테이너)를 써야 하나요? - 컨테이너를 사용해야 하는 이유

컨테이너는 서버 애플리케이션을 배포하고 서버를 운영하는 표준적인 기술이 되어가고 있습니다. 하지만 처음 사용해본다면 그 장점이 잘 와닿지 않을 수도 있습니다. 왜 굳이 도커 컨테이너를

www.44bits.io

 

2023.02.11 - [Web/Dev(ML)Ops] - ubuntu에서 도커 설치하기

 

ubuntu에서 도커 설치하기

$ sudo apt-get update apt-get update는 설치된 패키지를 업데이트하는 것이 아닌 설치 가능한 패키지 리스트를 업데이트 하는 것이다. repository 설정 $ sudo apt-get install ca-certificates curl gnupg lsb-release $ curl -f

dongineer.tistory.com

docker는 설치 됐다고 가정.

 

mysql 설치

원하는 버전을 8.0.30부분에 넣어준다. 기입하지 않으면 최신버전 이미지 가져옴.

$ sudo docker pull mysql:8.0.30

mysql 실행

$ sudo docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD={비밀번호} --name {컨테이너 이름} mysql:8.0.30 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
  • d : (detach) daemon으로 실행
    • 백그라운드에서 컨테이너 실행, 
  • p : HOST OS의 3306포트 : 컨테이너 내부 3306 포트
    • 호스트와 컨테이너 포트포워딩
  • e : MYSQL_ROOT_PASSWORD 설정.
  • name : 컨테이너 이름
  • mysql:8.0.30 : 이미지 이름

mysql 컨테이너 확인

$ sudo docker exec -it {컨테이너 이름} bash 
$ mysql -u root -p 

password : {설정한 MYSQL_ROOT_PASSWORD 입력}
  • -i, --interactive
    • 표준 입력(stdin)을 활성화하며, 컨테이너와 연결되어 있지 않더라도 표준 입력을 유지
    • 보통 이 옵션을 사용하여 Bash 에 명령을 입력
  • -t, --tty
    • TTY 모드(pseudo-TTY)를 사용
    • Bash를 사용하려면 이 옵션을 설정
    • 이 옵션을 설정하지 않으면 명령을 입력할 수는 있지만, 셸이 표시되지 않음

 

Nginx 설치

$ sudo apt install Nginx 

certbot을 통한 인증서 발급

$ apt-get install python3-certbot-nginx 
$ certbot certonly --nginx -d {myDomainName} 

/etc/letsencrypt/live/{myDomainName}/

  • cert.pem
  • chain.pem
  • fullchain.pem
  • privkey.pem

4개의 파일이 존재하는지 확인

Nginx

$ touch /etc/nginx/conf.d/{원하는 이름}.conf
$ sudo vim /etc/nginx/conf.d/{원하는 이름}.conf
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    root /var/www/html;
    server_name example.com www.example.com;
}
$ nginx -t
$ sudo service nginx restart

 

$ sudo vim /etc/nginx/sites-available/{원하는 이름}.conf

server {
  listen 80; #80 port(HTTP)로 받을때
  server_name <도메인 주소>; # 없을경우 localhost
  return 301 https://<도메인 주소>$request_uri;
}
server {
  listen 443 ssl http;
  server_name <도메인 주소>;

  # ssl 인증서 적용하기
  ssl_certificate /etc/letsencrypt/live/<도메인 주소>/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/<도메인 주소>/privkey.pem;

  location / {
    proxy_pass http://localhost:3000;
  }

  location /api { # location 이후 특정 url을 처리하는 방법을 정의
    proxy_pass http://localhost:8081; # Request에 대해 어디로 리다이렉트하는지
    proxy_redirect off;
    charset utf-8;

    proxy_http_version 1.1;
  }
}

location, proxy_pass에서 각 마지막 위치의 슬래쉬 여부에 따라 꼬일 수 있으니 파악하고 사용하기.
~^ 등의 표현식 제공

sites-enabled에 심볼릭 링크 생성

$ sudo ln -s /etc/nginx/sites-available/{원하는 이름}.conf /etc/nginx/sites-enabled

테스트 후 실행

$ sudo nginx -t

$ sudo service nginx start
# 설정을 변경했다면 꼭 sudo service nginx restart를 진행해주자...

https://docs.openvidu.io/en/2.19.0/deployment/ce/on-premises/

 

OpenVidu Docs

OpenVidu is deployed in production as a set of Docker containers managed with Docker Compose. You can deploy OpenVidu in any modern Linux distribution. You need root permissions to deploy OpenVidu. The recommended folder to install OpenVidu is /opt. Every

docs.openvidu.io

{} 내부에 있는 부분은 상황에 맞게 적어주세요.

 

$ sudo su
$ cd /opt
$ curl https://s3-eu-west-1.amazonaws.com/aws.openvidu.io/install_openvidu_latest.sh | bash

# latest를 원하지 않는다면 원하는 버전 latest자리에 기입
$ cd /opt/openvidu
$ vim .env

.env

 

DOMAIN_OR_PUBLIC_IP={myDomainName} #개인의 발급 받은 domain이 있다면 적어주자
OPENVIDU_SECRET={mySecret} #뭘 적든 자유
# CERTIFICATE_TYPE의 경우 letsencrypt를 통해 인증 받았다면 letsencrypt를 적고 밑에 이메일을 적어주자.
HTTP_PORT= {사용가능한 포트}
HTTPS_PORT= {사용가능한 포트}
포트 번호를 설정하는 부분이 있는데 사용가능한 포트를 기입하고 저장하자.
$ ./openvidu start

$ # = /opt/openvidu/openvidu start
실행 후 ctrl+c를 통해 백그라운드에서 실행할 수 있다. 

실행 후 브라우저에서 {기입했던 myDomanName}:{기입했던 사용가능한 포트}로 확인할 수 있다. 
$ ./openvidu stop

 

 

 

$ sudo apt-get update

apt-get update는 설치된 패키지를 업데이트하는 것이 아닌 설치 가능한 패키지 리스트를 업데이트 하는 것이다.

repository 설정

$ sudo apt-get install ca-certificates curl gnupg lsb-release

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

$ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Docker Engine 설치

$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

docker community edition, docker clie, containerd.io 설치

 

http://containerd.io란?

설치 확인

$ docker --version

Docker-compose 설치

$ sudo apt install jq

jq란? 리눅스에서 .json파일의 내용을 검색할 때 사용하는 툴.

$ VERSION=$(curl --silent https://api.github.com/repos/docker/compose/releases/latest | jq .name -r)

$ DESTINATION=/usr/local/bin/docker-compose

$ sudo curl -L https://github.com/docker/compose/releases/download/${VERSION}/docker-compose-$(uname -s)-$(uname -m) -o $DESTINATION

$ sudo chmod 755 $DESTINATION

설치 확인

$ docker-compose -v

+ Recent posts