배포 자동화 (feat. blue green 배포) - boostcamp-2020/Project12-B-Slack-Web GitHub Wiki

배포 자동화의 목표

배포 자동화 도입의 가장 큰 이유는 개발자가 배포 과정을 신경쓰지 않으며 온전히 개발에만 집중할 수 있도록 하는 것입니다.

개발자가 코드를 작성하고 Github에 PR을 보낸 후 Merge가 되면 해당 코드를 배포 서버에서 다운받고 빌드 및 배포를 하고 이를 팀원에게 알리는 과정이 필요합니다. 이 과정을 수동으로 진행하면다면 human error의 발생 여지가 있으며 번거로운 과정을 거쳐야 하기에 정해진 기간동안 개발에만 집중하기도 벅찬 상황에서 통합 및 배포에 피로함을 느낄 수 있습니다.

또한 저희의 프로젝트는 실시간 채팅이 주요한 목표였기에 develop branch를 이용한 pubilc ip를 가지는 개발용 서버를 만든 다음에 다른 사용자간 실시간 동작 기능을 테스트해볼 필요가 있기에 더욱 잦은 배포가 필요했습니다.

이를 개선하기 위해 아래와 같이 설계된 배포 자동화를 도입하여 자동화된 지속적 통합, 지속적 배포를 활용할 수 있도록 진행했습니다.

ccc

지속적 통합(CI)

Jenkins를 선택한 이유

저희 팀은 CI 도구로 Jenkins를 선택했습니다.

CI 도구를 선택할 때 고려해볼 도구는 Travis / Github Action / Jenkins가 있었습니다.

이 중 Jenkins를 선택한 이유는 경험적인 측면에서 가장 도움이 된다는 생각을 했습니다.

TravisCI는 Private Repository 를 이용할 경우 유료라는 이야기를 들었기에 사용 경험을 쌓는 면에서 제외를 시켰으며, github Action보다는 Jenkins가 널리 사용되고 많은 자료가 있기에 경험에 도움이 되리라 생각했습니다.

GitHub 연동

Jenkins에서 생성한 프로젝트에 Private Key를 등록하고 Github Repository에 Public Key를 등록하여 Github와 Jenkins를 연동할 수 있도록 설정합니다.

그리고 Github에서 webhook을 설정해서 특정 이벤트(merge)가 발생하면 Jenkins로 webhook을 보내서 변경된 코드를 받을 수 있도록 설정했습니다.

ddd

slack alert 연동

새로운 코드를 배포한다면 빌드 시작 및 성공/실패 여부를 팀원에게 항상 알릴 필요가 있다고 생각했습니다.

이를 만약 수작업으로 한다면 이 또한 계속적으로 성공/실패 여부를 확인하고 완료되었을 때 팀원에게 전파한다면 소모적인 일이라 생각을 했기에 slack을 연동해서 빌드 시작 및 성공/실패 여부를 알릴 수 있도록 설정했습니다.

eee

지속적 배포(CD)

blue/green 배포

사용자가 서비스를 끊기지 않고 이용할 수 있도록 무중단 배포를 하기 위해 사용한 전략은 blue/green 배포였습니다.

blue/green 배포 전략을 사용한 이유는 서버를 동작시키기 위해 하나의 컨테이너만을 이용하는 상황이기 때문에 자원이 2배로 필요해도 유의미하지 않았으며, 한 대씩 점차 배포하는 rolling update의 경우 하나의 컨테이너만을 활용하는 저희의 프로젝트에 적합하지 않았습니다. 또한 일부 트래픽만을 분산시켜서 오류여부를 판단하는 Canary 배포 또한 작은 프로젝트 규모인 black과는 적합하지 않다고 판단했습니다.

그리고 이런 blue/green 배포 전략을 적용하기 위해 선택한 도구는 docker, docker-compose, nginx입니다.

  • docker는 이미 유용하기로 유명한 도구입니다. host os에 설치된 개발환경에 종속되지 않으며, 가볍고 빠른 실행 속도 등이 매력적인 도구이기에 꼭 경험을 쌓고 싶었습니다.
  • docker-compose는 이러한 docker 컨테이너를 정의를 작성해서 한 번에 많은 컨테이너들을 작동시킬 수 있는 툴입니다. black 프로젝트는 규모가 작기에 blue, green 컨테이너를 띄울 때 하나의 컨테이너만을 띄우지만 확장성을 고려해서 여러개의 host를 띄우는 것을 생각했기에 docker-compose를 선택했습니다.
  • 그리고 각 포트가 다른 blue/green 컨테이너로 트래픽이 가도록 로드밸런싱을 할 수 있기 위해 nginx를 활용했습니다.

fff

위 그림과 같이 구 버전인 blue 컨테이너로는 트래픽이 가지 않고 새롭게 배포된 버전인 green 컨테이너만으로 트래픽이 갈 수 있도록 nginx가 로드밸런싱을 하고 있습니다.

deploy script

DOCKER_APP_NAME=webslack

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

if [ -z "$EXIST_BLUE" ]; then
    echo "blue up"
    docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d

    sleep 10

    docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
else
    echo "green up"
    docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d

    sleep 10

    docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
fi

위 스크립트와 같이 blue 컨테이너가 띄어있다면 새롭게 배포된 코드를 이용해 green 컨테이너를 up 시키고, 10초 뒤에 blue 컨테이너를 down시킵니다. 만약 blue 컨테이너가 내려가있는 상황이라면 blue 컨테이너를 up시키고, 10초 뒤 green 컨테이너를 down시킵니다.

docker-compose.yml

  • docker-compose.green.yml
version: '3'
  
services:
  webslack:
    image: webslack
    ports:
      - "8081:3000"
  • docker-compose.blue.yml
version: '3'
  
services:
  webslack:
    image: webslack
    ports:
      - "8082:3000"

yml파일은 간단하게 설정되어있으며 blue,green에 따라 포트만 다르게 설정했습니다.

nginx.conf

http {
  upstream webslack {
      server localhost:8081 max_fails=10 fail_timeout=10s;
      server localhost:8082 max_fails=10 fail_timeout=10s;
  }

  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  # server_tokens off;

  server {
          listen 3000;

          access_log /var/log/nginx/webslack-nginx.log;
          error_log /var/log/nginx/webslack-nginx-error.log;

          proxy_max_temp_file_size 0;
          proxy_buffering off;

          client_max_body_size 100M;

          root /usr/src/app/public;

          location / {
              proxy_pass http://webslack;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
              proxy_set_header Host $http_host;
              proxy_buffering            off;
              proxy_buffer_size          128k;
              proxy_buffers              8 256k;
              proxy_busy_buffers_size    256k;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection "upgrade";

          }
        }

nginx 설정파일입니다.

위와 같이 설정함으로써 3000번 포트로 들어온 트래픽을 webslack upstream중 health check를 해서 살아 있는 컨테이너로 트래픽을 전달할 수 있도록 로드밸런싱을 설정했습니다.

느낀 점

이로써 목표했던 CI/CD 및 blue/green 배포 설정을 완료했습니다.

이 작업을 하면서 느꼈던 점이 있습니다.

편하다

CI/CD를 구축한 후 좋은 느낀점은 편하다였습니다. 매번 배포 서버에서 새롭게 배포된 코드를 수작업으로 최신화하고, 빌드 및 설정을 해주어도 되지 않았으며 1,2분의 시간만 소요됐고 이 결과 또한 slack으로 알림을 받을 수 있었기에 온전히 개발에만 몰두 할 수 있었습니다.

특히, 팀원의 "동훈님, 이거 무척 편리하고 신기해요!"라는 말에는 보람을 느낄 수 있었습니다.

아직 CI라고 부르기엔 부족하다

3명의 팀원에서 진행했기에 필수 기능 개발에 벅차서 테스트 코드를 작성하지 못했으며 이는 테스트 자동화가 주된 목표인 CI를 온전히 활용하지 못했습니다.

마무리

평소 Back-end, DevOps, Cloud에 관심이 있었기에 이 작업은 행복한 일이었습니다.

항상 관심이 있고 봐왔었지만 제대로 된 개발 환경에 적용하지는 못했던 CI/CD와 docker인데 이번에 인프라 관련을 팀에서 전담하게 되면서 실컷 할 수 있었고 소중한 경험이었습니다.

특히 이 경험을 하면서 팀원들에게 편리하다는 말을 들을 때가 팀원에게는 내색하지 않았지만 가장 보람참을 느꼈던 순간이었습니다.

그러나 반대로 아직 갈 길이 멀다고도 느꼈습니다. 위의 내용을 작성하면서도 Jenkins를 제대로 사용하고 있었을까? docker를 제대로 사용했을까? nginx는? docker-compose는? 이라는 의문은 지난 4주 내내 머릿속을 떠나지 않는 생각이었습니다. 그리고 실제로도 제대로 사용하지 않는 부분이 많았습니다.

이러한 마음을 느끼는 만큼 앞으로도 지금과 같은 의문이 들지 않도록 배우면서 발전하고 성장해야겠다는 마음이 더 강해졌었습니다. 또한 해당 프로젝트에서는 원래 k8s를 활용해보고 싶었지만 이 또한 아쉬움이 남았습니다. 이 경험이 이후의 k8s 그리고 또 다른 경험에 밑거름이 되리라 생각하고 있습니다.