개발이란건 어쩌면…

완벽하다고 생각했던 코드는 조금만 시간이 지나서 다시 보면 왜이렇게 헛점 투성이 인걸까요?
개발이란건 어쩌면 삽질의 연속이 아닐까요?
이번에 무중단 배포 적용 진행하는 과정 또한 그러한 시행착오의 연속이었습니다.
완벽했다고 생각한 1차 배포기는 아래 링크에서 확인 가능합니다.

CI/CD 무중단 배포 POS Projcect 적용기 1탄 바로가기

첫번째 삽질

지난번 적용기의 핵심은 하나의 서버에 도커 컨테이너를 2개 띄워둔 상태에서 사용하지 않는 컨테이너를 최신상태로 recreate 한 후에 배포한다는 개념이었습니다.

1. before deploy
2. green container recreate
3. attach green container

무중단 배포 1탄에서 이 구성으로 진행했습니다. pos 웹서버에 적용했을때는 크게 문제가 없어서 porting api / common api에 확장 적용하였습니다 하지만 얼마 지나지 않아 문제가 발생하기 시작했습니다.

첫번째 문제 발생

“주문입력 중 계속해서 대기하는 현상이 있다고 합니다!”

이런 이슈를 받고 확인해보니 사유는 기존에 도커 설정이 서버의 자원을 최대한 활용하도록 설정되어 있었던 것이 었습니다.
특히 메모리를 최대로 설정하고 프로세스를 최대 150개까지 생성할수 있도록 설정되어 있었는데
컨테이너 내에서 활용되는 프로세스는 메모리를 공유하지만 외부 컨테이너와는 메모리가 공유되지 못하여 메모리 점유 문제로 프로세스가 메모리 반환을 대기하면서 작업수행이 느려지는 결과로 나타났습니다. 포스에서는 이러한 세팅값이 달랐기 때문에 이슈가 없었던 것이지요.

이 문제를 해결하기 위한 아이디어는 두가지 였습니다.

1. container의 하드웨어 사용률을 50%로 감소시킨다.

가장 쉬운 접근이었습니다. 메모리를 반만 사용하도록 하는것입니다.
하지만 이는 하드웨어 자원의 반을 낭비하는 것이기도 하고
2번과 동일한 이유로 완전한 무중단 배포가 불가능하여 차용되지 못했습니다.

2. detach 된 container를 종료시킨다.

그렇다면 동작하지 않는 container를 종료시키면 어떨까요?
하지만 이것도 쉬운일이 아니었는데요
무중단 배포에서 blue / green 배포의 가장 큰 목적은 실행중인 프로세스의 응답 확보입니다.
따라서 배포 후 detach된 container의 프로세스 종료를 감시하여 프로세스가 종료된것을 확인하고 종료해야 합니다.

사실 이 아이디어가 괜찮다고 생각하여 프로세스를 감시 / 종료하는 스크립트를 작성해보기도 했는데요 여기서도 문제가 있었습니다.

‘프로세스 종료까지 시간이 오래걸려 메모리 점유 문제가 또 발생한다면?’
‘프로세스 종료 감시중 다시 배포가 일어난다면?’

이 방법으로는 중단배포의 가능성이 있고 메모리 문제도 완벽하게 해결하기 힘들다는 결론에 도달했습니다.

3. 구관이 명관. 예전방법으로 해볼까?

사실상 이 시점에서 blue / green 컨테이너를 분리 배포 하는 방법은 힘들겠다는 결론에 도달했습니다. 그래서 원점에서 부터 다시 생각해보기로 했죠.

docker가 나오기 전 셀메이트는 배포를 어떻게 하고 있었을까요?

아주 아주 예전(10년전?)에는 ftp로 직접 파일을 전송하는 방식으로 배포를 했습니다.
아주 심플한 배포 방법이지만 협업하는데 있어 문제가 많았죠
서버마다 파일의 최신상태를 확인하기가 힘들었고
중요한 파일을 덮어 써버리는 경우도 종종 있었습니다.
그러다 다른사람의 작업물을 날려버리는 상황도 발생했습니다.

작업물을 날린 흔한 개발자의 모습

형상관리 시스템(git)을 쓰기 시작하면서 우리는 서버에서 git을 활용하여 배포하기 시작했습니다.
clone된 경로를 서버에서 serving 하고 배포할때마다 그 경로에서 git pull 하는 방법입니다.
하지만 이 방법도 문제가 있었는데요 프로세스등에서 사용중인 파일을 업데이트 할 수 없어서 git pull이 계속해서 실패하는 현상이 발생했죠.

그래서 현재 sellmate의 legacy web / open api는 폴더 수준에서의 blue / green 방법을 사용합니다.
blue가 현재 서빙되는 경로라면 green 폴더에 git으로 소스를 최신화 하고 iis / apache의 경로를 변경하는 방법이죠.
(이 방법 또한 배포가 빠르게 일어나면 동일하게 git pull이 실패하는 경우가 발생합니다만 무중단 배포는 확실하게 보장됩니다.)

서론이 길었습니다만 여기서 아이디어를 따와서 현재의 docker에서 volume으로 host의 파일을 참조하게 한뒤 host에서 폴더수준의 blue / green 정책을 쓰면 되겠다는 결론에 도달했습니다.

두번째 삽질

기존 구성에서 php-fpm docker가 내부 파일이 아닌 외부 파일을 참조하도록 했습니다.
open api와 유사하게 symbolic link를 volume으로 참조하고 symbolic link를 최신 파일 경로로 변경한다는 컨셉입니다.

그림으로 보면 아래와 같습니다.


1. 배포전 symbolic link - volume설정을 통한 폴더 설정
2. green 폴더에서 git pull
3. symbolic link를 green으로 변경


이 방법은 되겠다고 생각했습니다.
센터장님과도 논의를 마쳤죠.
스크립트를 짜고 구성을 변경하여 적용했습니다.
기존에 사용하던 방식과 유사하여 큰 문제가 없을거라 생각했습니다.
지금 생각해보면 그게 방심을 했던 원인이었나 봅니다.

두번째 문제 발생

“팀장님 서버에 파일이 배포가 안된것 같습니다!!”

Docker는 Symbolic link를 Volume으로 설정할 수 없다.

이번 적용기의 가장 큰 삽질입니다.
실제 배포를 적용 한 후 코드가 적용되지 않았는데요.
그 이유는 volume으로는 symbolic link의 변경이 적용되지 않는다는 것입니다.

무슨말인고 하니 위 그림에서 volume으로 symbolic link인 ‘current’를 지정하였지만
실제로 container에서는 설정 당시 경로인 blue를 참조하며
symbolic link를 green으로 변경하더라도 volume의 경로는 blue 그대로입니다.

(관련문서 - 도커 볼륨으로 Symbolic-Link가 가능할까?)

여기까지 오니까 약간 오기가 생기더라구요. 어떻게든 이 문제를 해결하고 싶었습니다.
그래서 도커내부에 blue / green 폴더를 생성하고 container 내부에서 symbolic link를 활용하는 방법으로 진행하였습니다.
그림으로 보면 아래와 같습니다.


1. 배포전 blue/grren - volume설정을 통한 폴더 설정
2. green 폴더에서 git pull
3. symbolic link를 green으로 변경


주요하게 수정된 파일은 아래와 같습니다.

docker/php.Dockerfile

주요내용은 blue / green을 나누어 동일 구성으로 만드는겁니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
COPY --from=vendor /app/vendor /var/www/blue/vendor
COPY --from=vendor /app/vendor /var/www/green/vendor

...

ADD . /var/www/blue
ADD . /var/www/green

RUN chown -R www-data:www-data \
    /var/www/blue/storage \
    /var/www/blue/bootstrap

RUN chown -R www-data:www-data \
    /var/www/green/storage \
    /var/www/green/bootstrap

RUN composer dumpautoload --working-dir=/var/www/blue && \
    composer dumpautoload --working-dir=/var/www/green && \
    rm /usr/bin/composer && \
    ln -nfs /var/www/blue /var/www/current

docker/nginx.Dockerfile

주요내용은 front code를 blue / green 배포 하기 위한 구성 추가한것입니다. (이부분은 backend만 있는 api 서버의 경우 불필요 합니다.)

1
2
3
...
    mkdir /var/www/current && \
    ln -nfs /var/www/blue/public /var/www/current/public

docker/nginx.default.conf

주요내용은 nginx root 경로 변경한 부분입니다.

1
  root /var/www/current/public;

pos는 프론트 파일도 제공하고 있어 전체 root path를 변경하였지만
backend 전용서버의 경우 fastcgi를 사용하는 영역에서만 root path를 잡아줘도 됩니다.
porting nginx 설정파일 보기

1
2
3
4
location ~ \.php$ {
  root /var/www/current/public;
  ...
}

위 도커 이미지를 가지고 아래와 같이 docker-compose를 구성하였습니다.

docker-compose.production.yml

주요내용은 blue / green에 동일한 volume 설정입니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  volumes:
      - ./blue/:/var/www/blue      
      - /var/www/blue/vendor
      - /var/www/blue/storage/logs
      - sellmatepos-app:/var/www/blue/storage/app      
      - sellmatepos-notice:/var/www/blue/storage/notice
      - sellmatepos-framework:/var/www/blue/storage/framework
      - sellmatepos-requestBoard:/var/www/blue/storage/requestBoard
      - sellmatepos-program:/var/www/blue/storage/program
      - ./green/:/var/www/green
      - /var/www/green/vendor  
      - /var/www/green/storage/logs
      - sellmatepos-app:/var/www/green/storage/app
      - sellmatepos-notice:/var/www/green/storage/notice
      - sellmatepos-framework:/var/www/green/storage/framework
      - sellmatepos-requestBoard:/var/www/green/storage/requestBoard
      - sellmatepos-program:/var/www/green/storage/program

포스 기존 구조상 좀 지저분한데요 이건 어쩔수가 없어 보입니다.
여기서 참고할만한 점은 중복되는 volume 설정의 경우 가장 나중에 작성된 내용이 우선한다는 점입니다.

1
2
3
4
5
  ...
      - ./blue/:/var/www/blue      
      - /var/www/blue/vendor #vendor는 도커내 파일을 사용하도록 설정
      - sellmatepos-app:/var/www/blue/storage/app #app은 외부 volume container를 사용하도록 설정
  ...

porting api의 구성은 여기서 확인해보세요.

이제 gitlab-ci 설정을 통해 자동으로 무중단 배포 되도록 해 보겠습니다.

anonymous volume

위 볼륨 지정시 콜론(:) 없이 경로만 지정된 설정이 있습니다.
저는 이를 도커내 파일 유지를 위해 설정했는데요(/var/www/blue/vendor)
이 설정으로 만들어진 anonymous volume은 컨테이너가 재생성되더라도 유지됩니다(!)
이것 때문에 저는 또또또 멘붕이 한번 왔었네요

예를 들면 패키지를 추가하여 composer.json을 업데이트 하면 이미지를 빌드하는 과정에서 vendor폴더를 갱신하게 되는데요
이렇게 이미지 내에서 vendor 폴더가 갱신되었더라도
위 설정대로 단순 docker-compose up 을 하게 되면 이전에 가지고 있던 파일을 사용해 버립니다
(일반적으로 볼륨으로 설정했을때 처럼요!)
그래서 docker-compose up에는 -V 옵션이 있는데요

-V, –renew-anon-volumes
Recreate anonymous volumes instead of retrieving data from the previous containers.

anonymous volume를 항상 재생성 한다는 옵션입니다.
서버에서는 해당 옵션을 넣어야 원하는 경로가 갱신되는 결과를 얻을수 있었습니다.

1. switch 스크립트 작성

먼저 스위치를 하기 위해서는 최근 배포 경로를 알아야 합니다.
최초에는 컨테이너 내부를 확인하는 식으로 구성 했지만 이래선 컨테이너가 내려가 버리면
최근에 어떤 경로를 배포 하고 있는지 알수 없습니다.
그래서 확정한 로직은 다음과 같습니다.

script/switch.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/bin/bash

set -e

APPCOLOR=blue

# 배포시 .appcolor 파일에서 최근 배포경로 확인하고 파일유무 / 최근 배포경로에 따라 변경되어야할 경로를 추출
if [ -f ".appcolor" ]
then
    APPCOLOR="$(cat .appcolor)"
else
    APPCOLOR="blue"
fi

echo "app to be ${APPCOLOR}"

if [ "$(docker ps | grep pos-php)" == "" ]
then
    echo "pos-php is not running"
    exit 0
else
    # 컨테이너 내부 경로 파악하기
    PHP_CURRENT=$(docker exec pos-php ls -al | grep current | cut -d '>' -f2 | sed -e 's/^[[:space:]]*//')

    #switch php
    if [[ "$PHP_CURRENT" =~ "$APPCOLOR" ]]
    then
        echo "current php already $APPCOLOR"
    else
        echo "switch php to $APPCOLOR"
        docker exec pos-php ln -nfs /var/www/$APPCOLOR /var/www/current
        if [ "$(docker exec pos-php ls -al | grep current)" != "" ]
        then
            echo "php cache clear!"
            docker exec --workdir /var/www/current pos-php php artisan cache:clear
        else
            echo "need php docker recreate"
        fi        
    fi    
fi

if [ "$(docker ps | grep pos-nginx)" == "" ]
then
    echo "pos-nginx is not running"
    exit 0
else
    NGINX_CURRENT=$(docker exec pos-nginx ls -al | grep public | cut -d '>' -f2 | sed -e 's/^[[:space:]]*//')

    #switch nginx
    if [[ "$NGINX_CURRENT" =~ "$APPCOLOR" ]]
    then
        echo "current nginx already $APPCOLOR"
    
    elif [ "$(docker exec pos-nginx ls -al | grep current)" != "" ]
    then
        echo "switch nginx to $APPCOLOR"
        docker exec pos-nginx ln -nfs /var/www/$APPCOLOR/public /var/www/current/public
    else
        echo "need nginx docker recreate"
    fi
fi

exit 0

2. gitlab-ci 수정

ci에서는 switch와 마찬가지로 .appcolor 파일로 git pull 할 경로를 선별하고
필요파일을 최신화 한다음 switch 스크립트를 실행하도록 했습니다.

gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
...
  script:
    - TARGET="blue" && [[ "$(ssh deployer@${SERVER_IP} "cat $APP_DIR/.appcolor")" =~ "blue" ]] && TARGET="green" #대상폴더 선별
    - RELEASE_DIR="$APP_DIR/$TARGET"
    - ssh deployer@${SERVER_IP} "([ ! -d "$RELEASE_DIR" ] && (echo "Cloning repository to $TARGET" &&
      git clone -b $CI_COMMIT_BRANCH --depth=1 git@gitlab.corp.sellmate.co.kr:${CI_PROJECT_PATH}.git $RELEASE_DIR && #대상폴더가 없으면 clone
      echo 'Allow anonymouse write permission on cache folder' &&
      chmod -R a+w $RELEASE_DIR/bootstrap/cache) ||
      (echo "Git Pull at $TARGET" && cd $RELEASE_DIR && git pull origin $CI_COMMIT_BRANCH && cd ~)) && #대상폴더가 있으면 git pull
      echo "$TARGET" > '${APP_DIR}/.appcolor' && 
      cp $APP_DIR/$TARGET/docker-compose.yml $APP_DIR && 
      cp $APP_DIR/$TARGET/docker-compose.$ENV.yml $APP_DIR && 
      cp $APP_DIR/$TARGET/scripts/switch.sh $APP_DIR && #필요파일 최신화
      cd $APP_DIR && ./switch.sh" #스크립트 실행
...

마치며…

정말… 많은 시행착오가 있었던 프로젝트였습니다.
사실 이정도로 힘들게 작성될줄은 꿈에도 몰랐어요.
실제로 이제 됐어! 라고 생각한 실서버 무중단 배포가 한 4차정도 일어났습니다… 배포는 되었지만 switch가 제대로 되지 않아서 문제가 되었는데요
정리해보면 다음과 같아요

  1. volume을 host의 symbolic link로 설정했을 경우 symbolic link의 대상경로로 지정된다. (symbolic link를 변경해도 변경되지 않음)
  2. blue에서 composer dumpautoload를 먼저하고 green으로 복사하면 green에서도 blue 경로를 바라보는 현상 -> 개별 처리하도록 하여 해결(autoload로 생성된 cache 파일이 문제일것으로 예상됨)

하지만 개인적으로 얻은것도 많았던 프로젝트였는데요 이번 프로젝트를 하면서 많이 배운건

  1. symbolic link에 대한 이해
  2. docker container / volume container에 대한 이해
  3. nginx에 대한 이해
  4. gitlab-ci의 동작 방식과 설정파일 작성 방식
  5. 쉘스크립트 작성 방식

써놓고 보니까 무중단 배포 하나에 알아야 하는 내용이 많은것 같기도 하네요.
이번에 완료된 내용은 common-api에도 적용될 예정입니다.

문의 사항이나 틀린점 / 좋은 의견 환영합니다!