CI/CD Jenkins & Canary release

2026. 3. 31. 17:14개발공부

CI/CD 파이프라인을 구성하려면 여러 방법이 있다.

오늘은 그 중 한가지 Jenkins와 도커를 활용해서 Canary release를 구성해보자.

준비물로는 리눅스 서버 둘을 사용할 것이다.

우선 코드는 다음과 같다
https://github.com/HappyTanuki/canary_test.git

 

GitHub - HappyTanuki/canary_test

Contribute to HappyTanuki/canary_test development by creating an account on GitHub.

github.com

Server.js

const http = require("http");

const PORT = process.env.PORT || 8080;
const COLOR = process.env.COLOR || 'blue';
const VERSION = 'V2';

http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`[${VERSION}] 현재 응답 서버 식별: ${COLOR} (포트: ${PORT})\n`);
}).listen(PORT, () => {
    console.log(`서버 가동 완료 (PORT: ${PORT})`);
});

default.conf

include /etc/nginx/conf.d/upstream.inc;

server {
    listen 80;

    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
    }
}

nginx/Dockerfile

FROM nginx:alpine
COPY default.conf /etc/nginx/conf.d/default.conf
RUN echo "upstream backend { server app-blue:8080; }" > /etc/nginx/conf.d/upstream.inc

Dockerfile

FROM node:18-alpine

WORKDIR /usr/src/app

COPY server.js .

# 가동시점이 다르다, 이거와 ENTRYPOINT는 실행시, RUN은 빌드 시
CMD ["node", "server.js"]

 

이제 다음 명령어로 기본적으로 서버 배포 환경을 먼저 구성한다:

docker network create canary-net
cd nginx
docker build -t my-nginx .
docker run -d --name nginx-proxy --network canary-net -p 8088:80 my-nginx
cd ..
docker build -t my-canary-app .
docker run -d --name app-blue --network canary-net -e PORT=8080 -e COLOR="🔵 BLUE" my-canary-app

 

여기까지 잘 수행되었다면 상황은 다음과 같이 된다:

happytanuki@tanukiworld:~/canary_test$ docker ps
CONTAINER ID   IMAGE                                          COMMAND                  CREATED             STATUS             PORTS                                     NAMES
aba1b7bab705   my-nginx                                       "/docker-entrypoint.…"   52 minutes ago      Up 52 minutes      0.0.0.0:8088->80/tcp, [::]:8088->80/tcp   nginx-proxy
78e274dc3d24   my-canary-app                                  "docker-entrypoint.s…"   About an hour ago   Up About an hour                                             app-blue

 

nginx reverse proxy를 설정하여 이 서비스를 외부에서 접속할 수 있도록 설정한다:

happytanuki@tanukiworld:~$ cat /etc/nginx/sites-available/canary_test
server {
    server_name canary.happytanuki.kr;
    set $jenkins_addr http://127.0.0.1:8088;

    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-Frowarded-Proto $scheme;

    # set timeout
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    send_timeout       600s;

    location / {
            proxy_pass $jenkins_addr;
    }


    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/canary.happytanuki.kr/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/canary.happytanuki.kr/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}
server {
    if ($host = canary.happytanuki.kr) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    server_name canary.happytanuki.kr;
    listen 80;
    return 404; # managed by Certbot


}

이제 접근이 되는지 확인해보자

잘 되었다면 다음 단계로 진행한다

 

이제 server.js를 교체하는데 canary 방식을 사용하기 위해서 jenkins를 준비해 보겠다.

나는 truenas scale 환경에 jenkins를 올려서 설정을 해 주었다.

truenas apps 패널
nginx 리버스 프록시 세팅

/github-webhook/에만 접속을 허용함으로써 보안성을 챙기고 기본적인 https 설정을 해 주고

jenkins 유저를 만들고 배포 환경(다른 환경)에 접속 권한을 주었다.


jenkins docker 내부에 접속해서 ssh-keygen을 실행하여 키 페어를 만들고 공개키를 배포 서버의 authorized_keys에 공개키를 등록한다.

 

한편, github에서도 프로필 > settings > developer settings > personal access tokens >
fine-grained personal access token을 발급받는다.

웹훅 수신과 clone을 위한 권한 설정

권한 설정은 위와 같이 설정하여 토큰을 발급받아 기억해 두고 github repository > settings > webhooks 로 이동한다.

webhooks

Add webhook을 눌러 새 웹훅을 추가한다.

 

이런 식으로 설정한다 (Payload URL은 자신의 서버 도메인을 연결하자)

Add webhook을 눌러주면 이제 github가 로컬의 jenkins에게 알림을 주게 된다.

 

jenkins에 새 프로젝트 프리스타일로 만든다

자신의 git 리포 주소를 넣어주고 오른쪽의 add를 눌러 credential을 추가한다.

username with password를 선택해 준다.

username에 자신의 깃허브 계정명을 넣어주고

아까 발급받았던 토큰을 password에 넣어준다

빌드할 브랜치를 선택해 주고

깃허브 웹훅에 반응하도록 GitHub hook trigger for GITScm polling을 체크해 준다

그리고 Add build step을 해서 다음과 같은 쉘 스크립트 빌드 단계를 입력해 준다.

#!/bin/bash

REMOTE_ENVIROMENT="-p 8000 happytanuki.kr"
REMOTE_PATH="~/canary_test"

function update_nginx_weight() {
    local BLUE=$1
    local GREEN=$2
    echo ">> 트래픽 비율 변경 중... Blue(${BLUE}%) / Green(${GREEN}%)"

    # weight가 0인 서버는 설정에서 아예 제외합니다! (Nginx 에러 방지)
    CONF="upstream backend { "
    [ "$BLUE" -gt 0 ] && CONF="${CONF} server app-blue:8080 weight=${BLUE}; "
    [ "$GREEN" -gt 0 ] && CONF="${CONF} server app-green:8081 weight=${GREEN}; "
    CONF="${CONF} }"

	ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
    ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker exec nginx-proxy sh -c \"echo '$CONF' > /etc/nginx/conf.d/upstream.inc\""
	ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
    ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker exec nginx-proxy nginx -s reload"
}

# 1. 지금 켜져있는 서버가 누군지 검사합니다.
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
IS_BLUE=$(ssh ${REMOTE_ENVIROMENT} docker ps -q -f name="^app-blue$")

if [ -n "$IS_BLUE" ]; then
    CURRENT="blue"; TARGET="green"; TARGET_PORT=8081; TARGET_COLOR="🟢 GREEN"
else
    CURRENT="green"; TARGET="blue"; TARGET_PORT=8080; TARGET_COLOR="🔵 BLUE"
fi

echo "🚀 새로운 버전 [${TARGET}] 구동 시작!"

# 2. 최신 코드로 도커 이미지를 굽습니다(build).
echo "building canary app"
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker build -t my-canary-app ."

# 3. 혹시 예전에 쓰다 남은 타겟 컨테이너가 있다면 미리 삭제합니다.
echo "deleteing canary app"
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker rm -f app-$TARGET 2>/dev/null"

# 4. 새 컨테이너를 실행합니다! (핵심: --network 로 같은 가상망에 묶어줍니다)
echo "running canary app"
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker run -d --name app-$TARGET \
  --network canary-net \
  -e PORT=$TARGET_PORT \
  -e COLOR=\"$TARGET_COLOR\" \
  my-canary-app"

# 5. Health Check: Nginx가 새 서버랑 대화가 잘 되는지 확인합니다.
sleep 5
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
RESPONSE=$(ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker exec nginx-proxy sh -c \"wget -qO- http://app-$TARGET:${TARGET_PORT}\"")
if [ -z "$RESPONSE" ]; then
    echo "❌ 실패! 새 서버가 제대로 켜지지 않았습니다. 롤백합니다."
	ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
    ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker rm -f app-$TARGET"
    exit 1
fi

# ==========================================
# 6. 점진적 트래픽 전환 (Canary Stages)
# ==========================================
echo "✅ [1단계] 10% 카나리 오픈 (15초 대기)"
if [ "$TARGET" == "green" ]; then update_nginx_weight 90 10; else update_nginx_weight 10 90; fi; sleep 15

echo "✅ [2단계] 50% 트래픽 전환 (15초 대기)"
if [ "$TARGET" == "green" ]; then update_nginx_weight 50 50; else update_nginx_weight 50 50; fi; sleep 15

echo "🎉 [3단계] 100% 완전 전환!"
if [ "$TARGET" == "green" ]; then update_nginx_weight 0 100; else update_nginx_weight 100 0; fi

# 7. 이제 쓸모없어진 구버전 컨테이너를 삭제(rm)합니다!
ssh-keyscan ${REMOTE_ENVIROMENT} > ~/.ssh/known_hosts
ssh ${REMOTE_ENVIROMENT} "cd ${REMOTE_PATH} && docker rm -f app-$CURRENT"

 

여기까지 완료되었다면 save를 눌러 빠져나와 주면 main 브랜치에 push하거나

지금 빌드 버튼을 눌러주면

빌드와 배포가자동으로 되기 때문에

배포 url에 일정 비율만큼 green blue가 트래픽을 나눠받는 걸 f5를 연타함으로써 확인해볼 수 있다