Inspiration

신승혁님께서 무중단 배포 관련하여 리서치한 내용에서 영감을 받아 실제 포스 웹 프로젝트에 적용해본 작업을 정리해보았습니다. 승혁님 감사합니다. 역시 내 멘티
승혁님의 원글은 아래 링크에서 확인가능합니다.

CI/CD 무중단 배포 (with jenkins,docker,nginx)

오타 / 의견 환영합니다! 댓글이나 DM으로 알려주세요!

docker-compose 로 blue/green 구성하기

현재의 POS 프로젝트 서버는 Nginx + Php-fpm 도커 컨테이너 구성으로 되어 있습니다. Blue/Green 배포 전략을 위해서는 Blue/Green 역할의 Php-fpm 컨테이너가 필요했습니다.

/docker/docker-compose.production.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
services:
  pos-php:
    image: gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/web
    container_name: pos-php
    build:
      context: .
      dockerfile: docker/php.Dockerfile
    env_file:
      - .env
    working_dir: /var/www
    # volumes: 생략      
 
  pos-nginx:
    container_name: pos-nginx
    image: gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/nginx
    # volumes: 생략      
    # ports: 생략      

volumes:
  #생략

기존의 production 구성입니다. 실제로는 docker-compose.yml + docker-compose.production.yml의 조합으로 이루어지지만 설명을 위해 필요한 부분만 남겼습니다.
이 구성은 아래와 같이 변경되었습니다.

/docker/docker-compose.production.yml

 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
services:
  pos-php-blue:
    image: gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/web
    container_name: pos-php-blue
    build:
      context: .
      dockerfile: docker/php.Dockerfile
    env_file:
      - .env
    working_dir: /var/www
    # volumes: 생략

  pos-php-green:
    image: gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/web
    container_name: pos-php-green
    build:
      context: .
      dockerfile: docker/php.Dockerfile
    env_file:
      - .env
    working_dir: /var/www
    # volumes: 생략
 
  pos-nginx:
    container_name: pos-nginx
    image: gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/nginx
    # volumes: 생략      
    # ports: 생략      

volumes:
  #생략

변경된 부분은 많지 않습니다. 동일한 이미지를 쓰는 service를 하나 추가하고 service 명과 container_name을 중복되지 않도록 했습니다.

변경된 구성에서 pos-nginx 서비스가 기본적으로 blue를 보도록 설정파일(docker/nginx.default.conf)을 아래와 같이 변경했습니다.

docker/nginx.default.conf

1
2
3
4
5
6
7
8
9
## 변경전
...
fastcgi_pass    pos-php:9000;
..
##

## 변경후
fastcgi_pass    pos-php-blue:9000;
##

이제 기본적인 구성 준비는 끝났습니다. 이걸 그대로 docker-compose up 하게 되면 pos-php-blue 컨테이너로 동작하는 서버가 될겁니다.

배포 스크립트 작성

무중단 배포의 핵심은 배포에 사용되는 php-fpm 서비스 추가로 하나더 띄우고 nginx의 설정 변경 후 nginx를 reload 하는것입니다.
요약하면 다음 순서되로 진행될것입니다.

  1. 현재 실행중인 php 컨테이너 확인 (blue 인지 green 인지)
  2. 최신 배포판을 현재 실행중이지 않은 서비스로 올리기
  3. nginx 설정 변경 (blue -> green or green -> blue) 후 reload

1. 현재 실행중인 php 컨테이너 확인하기

현재 php코드가 어떤 컨테이너로 서비스 되고 있는지 어떻게 알수 있을까요?
여러가지 방법이 있을수 있겠지만 nginx의 설정파일인 defaul.conf 파일을 보는 것이 가장 간편해 보였습니다.
위에서 변경한 바로 그 부분입니다.

이부분을 cli에서 확인해보면 다음과 같이 작성해 볼 수 있습니다.

1
2
docker exec -it pos-nginx grep -rw /etc/nginx/conf.d/default.conf -e 'fastcgi_pass'
        fastcgi_pass    pos-php-blue:9000;

만약 다른 fastcgi_pass 설정이 존재한다면 다른 방법을 써야 했겠지만
다행히도 현재는 하나밖에 없으니 이 방식을 활용 해보겠습니다.

이제 현재 서비스 되고 있는 컨테이너가 확인 가능하니 이걸 기준으로 스크립트를 만들어 보겠습니다.

도커 구성은 production 서버와 staging 서버가 네이밍/volume설정 등에서 약간 차이가 있는데요
그래서 compose 파일이 staging과 production용으로 나누어져 있습니다. 여기서는 production을 기준으로 설명해 보겠습니다.

deploy-php.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
echo "php deploy start"

echo "current blue green check"
fastcgi_pass="$(docker exec pos-nginx grep -rw /etc/nginx/conf.d/default.conf -e 'fastcgi_pass')"

if [[ "$fastcgi_pass" =~ "blue" ]]
then
    from=pos-php-blue
    to=pos-php-green
elif [[ "$fastcgi_pass" =~ "green" ]]
then
    from=pos-php-green
    to=pos-php-blue
else
    echo "blue green check error"
    exit 1
fi

echo "php service is $from"
echo "php service will be $to"

컨테이너가 제대로 실행중이라면 다음과 같이 출력될 것입니다.

1
2
php service is pos-php-blue
php service will be pos-php-green

이제 service중인 컨테이너와 앞으로 service 되어야할 컨테이너를 파악했습니다. 위 데이터를 바탕으로 $to의 서비스를 pull & up 해보겠습니다.

2. 최신 배포판을 현재 실행중이지 않은 서비스로 올리기

docker-compose 명령어 다음에 service 명을 붙여 원하는 서비스에만 적용 할 수 있습니다.

deploy-php.sh

1
2
3
4
5
...
echo "compose up [$to]"
docker-compose -f docker-compose.yml -f docker-compose.production.yml pull $to &&
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d $to
...

위 코드는 빌드된 최신버전의 php-fpm 이미지가 gitlab.corp.sellmate.co.kr:5005/sellmate-pos/web-v2/web repository에 push 된것을 전제합니다. (build 부분은 pos프로젝트에서 .gitlab-ci.yml 의 build 스텝을 참고하세요)

-f docker-compose.production.yml 해당옵션은 compose 파일을 지정하는것입니다. 위와 같이 파일이 여러개 지정하면 오버라이딩되어 실행됩니다.

이제 nginx 컨테이너 내부에서 설정을 변경하고 reload 시켜 보겠습니다.

3. nginx 설정 변경 (blue -> green or green -> blue) 후 reload

sed 명령어를 사용해 봤습니다. 조건에 맞게 파일내용을 대체(replace)하는 명령어 입니다.

deploy-php.sh

1
2
3
4
5
6
7
...
echo "nginx change [$from] to [$to]"
docker exec pos-nginx sed -i "s/$from:9000/$to:9000/" /etc/nginx/conf.d/default.conf

echo "reload [pos-nginx]"
docker exec pos-nginx nginx -s reload
...

이제 위 명령어를 실행하면 설정파일에서 fastcgi_pass 설정을 변경하고 nginx를 reload 하겠네요.

테스트를 위하여 1초에 한번 요청에 따라 sse응답을 생성하는 /sseTest 라우트를 작성하여 활용했습니다.
/sseTest?loop_cnt=1000 이렇게 하면 1000까지 응답을 발생시키는데요
배포 중 실행중인 컨테이너가 중단된다면 1000까지 응답이 작성되기전에 종료 될것입니다.

아래는 지금까지 작성된 deploy-php.sh의 전문입니다.

deploy-php.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
#!/bin/bash
echo "php deploy start"

echo "current blue green check"
fastcgi_pass="$(docker exec pos-nginx grep -rw /etc/nginx/conf.d/default.conf -e 'fastcgi_pass')"

if [[ "$fastcgi_pass" =~ "blue" ]]
then
    from=pos-php-blue
    to=pos-php-green
elif [[ "$fastcgi_pass" =~ "green" ]]
the
    from=pos-php-green
    to=pos-php-blue
else
    echo "blue green check error"
    exit 1
fi

echo "php service is $from"
echo "php service will be $to"

echo "compose up [$to]"
docker-compose -f docker-compose.yml -f docker-compose.production.yml pull $to &&
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d $to

echo "nginx change [$from] to [$to]"
docker exec pos-nginx sed -i "s/$from:9000/$to:9000/" /etc/nginx/conf.d/default.conf

echo "reload [pos-nginx]"
docker exec pos-nginx nginx -s reload

/sseTest?loop_cnt=1000을 호출하고
프로젝트 root 경로에서 deploy-php.sh를 실행하고 배포실행시 배포 완료 후 잘 동작하는것을 확인하였습니다.

4. nginx를 recreate하려면?

아직 해결해야할 문제가 있습니다. nginx 컨테이너가 recreate되면 최신상태를 잃게 됩니다.
컨테이너 내의 파일을 변경했기 때문입니다.

1
docker-compose -f docker-compose.yml -f docker-compose.production.yml up pos-nginx

위와 같이 코드를 실행했을때 실행전 nginx 설정이 green으로 되어 있었다면 기본값인 blue로 변경되어 버릴겁니다.

컨테이너 내 파일의 변경이 유지되길 원할 경우 volume을 사용합니다.
저는 아래와 같이 설정하여 conf 파일을 참조 하도록 하였습니다.

/docker/docker-compose.production.yml

1
2
3
4
5
6
7
...
pos-nginx:
    ...
    volumes:
      ...
      - ./docker/nginx.conf.d:/etc/nginx/conf.d
...

이제 ./docker/nginx.conf.d 경로에 default.conf 파일을 만들어주면 됩니다.
deploy-php.sh 상단에 다음과 같이 추가 하였습니다.

deploy-php.sh

1
2
3
4
5
6
7
8
9
#!/bin/bash

confFile=docker/nginx.conf.d/default.conf

if [ ! -f "$confFile" ]
then
    cp docker/nginx.default.conf ${confFile}
fi
...

docker/nginx.default.conf 파일은 기존에 image생성시 설정파일로 사용되는 파일로 volume 경로에 default.conf 파일이 없으면 기존 설정파일을 복사해주는 간단한 코드 입니다.

해당파일은 배포마다 변경되는 파일임으로 .gitignore도 추가해 봅시다.

./docker/nginx.conf.d/.gitignore

1
2
*
!.gitignore

이제 해당파일로 blue/green 체크를 하고 해당 파일의 설정을 변경해 보겠습니다.

deploy-php.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
echo "current blue green check"
--- 변경전

fastcgi_pass="$(docker exec pos-nginx grep -rw /etc/nginx/conf.d/default.conf -e 'fastcgi_pass')"

--- 변경후

fastcgi_pass="$(grep -rw ${confFile} -e 'fastcgi_pass')"

...

--- 변경전

echo "nginx change [$from] to [$to]"
docker exec pos-nginx sed -i "s/$from:9000/$to:9000/" /etc/nginx/conf.d/default.conf

--- 변경후

sed -i "s/$from:9000/$to:9000/" ${confFile}
...

완성된 deploy-php.sh는 다음과 같습니다.

deploy-php.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
#!/bin/bash
confFile=docker/nginx.conf.d/default.conf

if [ ! -f "$confFile" ]
then
    cp docker/nginx.default.conf ${confFile}
fi

echo "php deploy start"

echo "current blue green check"
fastcgi_pass="$(grep -rw ${confFile} -e 'fastcgi_pass')"

if [[ "$fastcgi_pass" =~ "blue" ]]
then
    from=pos-php-blue
    to=pos-php-green
elif [[ "$fastcgi_pass" =~ "green" ]]
the
    from=pos-php-green
    to=pos-php-blue
else
    echo "blue green check error"
    exit 1
fi

echo "php service is $from"
echo "php service will be $to"

echo "compose up [$to]"
docker-compose -f docker-compose.yml -f docker-compose.production.yml pull $to &&
docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d $to

echo "nginx change [$from] to [$to]"
docker exec pos-nginx sed -i "s/$from:9000/$to:9000/" /etc/nginx/conf.d/default.conf

echo "reload [pos-nginx]"
docker exec pos-nginx nginx -s reload

exit 0

5. ci에게 배포 작업 넘기기

해당 배포 작업을 ci가 하도록 해보겠습니다. 내용이 많지만 무중단 배포에 관련이 있는 배포시점 스크립트만 살펴 보겠습니다.

.gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

--- 변경전
deploy-1:
    ...
    script: ssh deployer@10.1.11.11 "cd ~/pos &&
        git fetch --all &&
        git reset --hard origin/master &&
        git pull origin master &&
        docker login -u '$CI_REGISTRY_USER' -p '$CI_REGISTRY_PASSWORD' $CI_REGISTRY &&
        docker-compose -f docker-compose.yml -f docker-compose.production.yml pull &&
        docker-compose -f docker-compose.yml -f docker-compose.production.yml up -d"
    ...

--- 변경후
deploy-php-1:
    ...
    script: ssh deployer@10.1.11.11 "cd ~/pos &&
        git fetch --all &&
        git reset --hard origin/master &&
        git pull origin master &&
        docker login -u '$CI_REGISTRY_USER' -p '$CI_REGISTRY_PASSWORD' $CI_REGISTRY &&
        scripts/deploy-php.sh"
    ...

pull & up 명령어를 실행하는 부분을 스크립트를 실행하도록 수정했습니다.

ci 관련하여 궁금하신분은 관련하여 리서치 해보세요. 공식문서는 요기 -> gitlab ci

저는 여기에 살을 붙여 production과 staging을 구분하는것도 추가 하여 스크립트를 완성 하였습니다.

deploy-php.sh 훑어보기

nginx 컨테이너 배포 스크립트도 작성하였지만 크게 추가된 내용은 없습니다.
deploy-nginx.sh 훑어보기

흥미로운 주제 였기 때문에 도커 구성을 변경 / 스크립트를 작성하고 ci에 적용하는 과정은 생각보다 재밌었습니다.

현재의 간소한 구성은 이와 같이 스크립트 만으로도 충분할것 같은데요
추후에는 서버 자원의 효율적 활용을 위해 쿠버네티스나 도커스웜의 오케스트레이션도 같이 리서치 해보고 적용해볼수 있으면 좋을것 같습니다.

추신

  1. 윈도우 환경에서는 wsl을 사용해야 쉘 스크립트 작성에 편하다.
  2. chmod +x 로 실행권한을 적용 해야 ci에서 실행 가능하다.
  3. 블로그는 지식의 공유 뿐만아니라 서로에게 동기부여가 되는 좋은 장인것 같다. 게시물이 많아졌으면 좋겠음!