마지막 CI/CD 작업이다.
하나의 이미지를 여러 인스턴스에 그리고 인스턴스 내 여러 컨테이너를 통해 실행하는 것이 목적이다.
Oracle 인스턴스를 통해 백엔드 배포하기
오라클 인스턴스 역시 리눅스 기반이기에 동일하게
- 인바운드 포트 열고 (지금은 테스트 용으로 전부 개방했지만 ec2에 있는 게이트웨이 인스턴스와 내 로컬 pc ip 만 허용시킬 것이다)
- 해당 포트로 서버 배포시키고
- 테스트해보기
큰 무리 없이 정상적으로 서버가 확인 되었다면 이제 다음으로 넘어가자
nginx를 통해 포트 없이 배포시킨 서버로 요청을 넘겨보자
docker compose를 통해 scale 값을 주고 여러 컨테이너를 띄우려면 해당 컨테이너가 받을 포트를 하나만 딱 정할 수 없다
즉, 컨테이너를 8080:8080 이런식으로 명시한 상태로 동일 이미지로 2개 띄워버리면 두 컨테이너가 충돌하기에 다른 방법이 필요하다.
8080 ~ 8088 이런식으로 포트 범위를 주는 방법도 있지만 특정 갯수를 내가 설정해야 하기에 유연하지 못하다고 생각한다.
그래서, docker compose에 nginx를 80:80으로 배포시키고 80으로 오는걸 proxy_pass로 be 서버들에게 보내는 것을 목적으로 한다.
그렇기에 우선 포트 없이 배포시킨 컨테이너를 Nginx를 통해 접근해보자.
events {
worker_connections 1000;
}
http {
upstream be-blue {
server be-blue:8080;
}
server {
listen 80;
location / {
proxy_pass <http://be-blue/>;
}
}
}
이런식으로 간단하게 nginx 설정 파일을 생성하고 저장해준다.
version: '3'
services:
be-blue:
image: sok5188/gyunpang-be:latest
environment:
CONTAINER_COLOR: "blue"
nginx:
image: nginx:latest
volumes:
- ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
restart: always
ports:
- "80:80"
docker compose 파일도 위처럼 설정해준다.
volume에 아까 작성했던 nginx 파일을 잡아주고 컨테이너 포트도 80:80으로 잡아서 80 포트로 오는 접근 모두를 8080으로 패싱하게끔 설정하자.
그리고 포트번호 없이 접속하게 되면
정상적으로 응답하는 모습을 볼 수 있다.
scale 옵션으로 여러 컨테이너를 띄워보기
sudo docker compose up -d --scale be-blue=3
위 명령을 통해 백엔드 서버를 여러개 올려본 후
log를 확인해보면
이런식으로 서버가 3대 올라가게 되고
api 호출 후 로그를 다시 보면 1,2 컨테이너가 응답을 받고 처리한 로그를 확인할 수 있다.
근데.. 3번 컨테이너는 정상적으로 올라가긴했지만 막상 api 요청을 처리하지는 않았다.
추측하기론, 라운드로빈 방식으로 api 요청을 돌린다고는 했으니 분명 1→2→3→1 이렇게 순차적으로 요청이가야 하는데.. api가 단순히 string만 반환하기에 3번째 서버까지 쓰지 않으려 하는 것 같기도 한데..
우선 추후에 서버가 응답을 보내고 비동기 이벤트로 쓰레드를 재우는 api를 만들어서 테스트를 다시 해봐야 할 것 같다.
여튼..여러 컨테이너가 띄워졌으니 이제 배포 자동화만 설정하면 끝이다.
Blue Green 컨테이너를 통한 무중단 배포하기
이제 진행해볼 단계에선
- 들어온 요청을 Blue Green 컨테이너로 동적으로 변환하기
- docker compose를 통해 nginx 컨테이너만 reload 시키기
- docker compose 를 통해 blue, green을 스케일을 적용한 상태로 개별로 올리고 내리기
- 위의 내용들을 바탕으로 github workflow를 작성하고 실행 스크립트 파일을 작성하기
크게 위 단계로 진행해보려 한다.
1.들어온 요청을 Blue Green 컨테이너로 동적으로 변환하기
gateway 처럼 포트가 정해진 컨테이너가 아니기에 조금은 다른 방식이 필요하다고 생각한다.
- 실패시도
- 로컬 변수 설정 → upstream에서 변수 사용
- http 블록에서 사용 불가
- map 모듈과 환경 변수설정을 통해 변수에 따라 upstream 동적 변경
- docker compose 설정 파일에 environment로 값 추가 → unknown variable 에러 발생 및 해결 시도 실패
- 설정 파일을 template으로 만들고 envsubt 실행??(gpt친구가 말해준거..) → invalid number of arguments 오류 → 이것저것 해봤지만 역시 실패
- 프록시 패스하는 부분을 변수로 설정하고 upstream을 blue,green 모두 잡기
- blue 와 green을 동시에 실행시키는 상황은 deploy 시점 잠깐 뿐이기에 green으로 업스트림을 보낼 수 없음 → 실패
- 로컬 변수 설정 → upstream에서 변수 사용
생각보다 단순하게 설정할 수 있다.
gateway 시 했던 것 처럼 배포 시점에 복사될 nginx.conf 파일을 생성하고 해당 파일에 덮어씌울 nginx_blue.conf , nginx_green.conf 를 생성하는 것이다.
즉, 기존에 작성했던 파일 그대로를 green 버전으로 생성만 하면 된다.
nginx_green.comf
events {
worker_connections 1000;
}
http {
upstream be-green {
server be-green:8080;
}
server {
listen 80;
location / {
proxy_pass <http://be-green/>;
}
}
}
docker-compose.yml
version: '3'
services:
be-blue:
image: sok5188/gyunpang-be:latest
environment:
CONTAINER_COLOR: "blue"
be-green:
image: sok5188/gyunpang-be:latest
environment:
CONTAINER_COLOR: "green"
nginx:
image: nginx:latest
volumes:
- ./nginx/config/nginx.conf:/etc/nginx/nginx.conf
restart: always
ports:
- "80:80"
이런식으로 nginx 컨테이너를 띄우는 시점에 nginx.conf 파일을 복사해가도록 설정하고 blue, green 상태에 맞춰 nginx.conf 를 바꿔 주면 된다.
2. docker compose를 통해 nginx 컨테이너만 reload 시키기
기존에는 인스턴스 자체에서 nginx를 실행시켰기에 인스턴스 터미널에서 reload를 시켰다.
docker compose로 구성된 nginx 컨테이너를 Reload를 시키려면 어떻게 해야 할 까 ?
또, 그렇게 reload해도 docker compose 설정 파일을 바탕으로 다시 volume을 잡아갈까 ?
우선 현재는 green이 라우팅 되고 있으니 이걸 blue로 변경해보자
sudo cp nginx_blue.conf nginx.conf
이제 우리가 nginx 컨테이너에 전달하는 nginx 설정 파일은 변경했으니 nginx 컨테이너를 reload해보면
sudo docker compose exec nginx(container name) service nginx reload
원하던 대로 변경된 것을 볼 수 있다.
아무래도 볼륨에 연결된 파일을 변경하면 컨테이너 내부 볼륨의 파일도 바로 변경되는 것 같아 보인다.
확인해보자
다시 green으로 nginx.conf를 덮어씌우고
sudo docker compose exec -it nginx /bin/bash
로 nginx bash로 들어가 nginx.conf를 확인해보면
생각했던대로 바로 변경된 것을 확인할 수 있다.
3.docker compose 를 통해 blue, green을 스케일을 적용한 상태로 개별로 올리고 내리기
간단하다 개별 컨테이너를 올리듯이 up해주고 —scale로 옵션만 넣어주면 된다.
sudo docker compose up -d be-green --scale be-green=3
sudo docker compose down be-green
이런 식으로 올리고 내리면 된다.
4.위의 내용들을 바탕으로 github workflow를 작성하고 실행 스크립트 파일을 작성하기
이제 필요한 모든 기능에 대해서 동작을 확인했으니 이걸 바탕으로 워크플로우와 스크립트 파일을 작성해보자
deploy.sh
#!/bin/bash
IS_GREEN=$(sudo docker ps | grep green) # 현재 실행중인 App이 blue인지 확인합니다.
if [ -z "$IS_GREEN" ]; then # blue라면
echo "### BLUE => GREEN ###"
echo "1. get green image"
sudo docker compose pull be-green
echo "2. green container up"
sudo docker compose up -d be-green --scale be-green=3
for cnt in {1..10}
do
echo "3. green health check..."
echo "서버 응답 확인중(${cnt}/10)";
REQUEST=$(curl <http://localhost/health>)
if [ -n "$REQUEST" ]
then # 서비스 가능하면 health check 중지
echo "health check success"
break ;
else
sleep 10
fi
done;
if [ $cnt -eq 10 ]
then
echo "서버가 정상적으로 구동되지 않았습니다."
exit 1
fi
echo "4. reload nginx"
sudo cp /home/ubuntu/nginx/config/nginx_green.conf /home/ubuntu/nginx/config/nginx.conf
sudo docker compose exec nginx service nginx reload
echo "5. blue container down"
sudo docker compose down be-blue
else
echo "### GREEN => BLUE ###"
echo "1. get blue image"
sudo docker compose pull be-blue
echo "2. blue container up"
sudo docker compose up -d be-blue --scale be-blue=3
for cnt in {1..10}
do
echo "3. blue health check..."
echo "서버 응답 확인중(${cnt}/10)";
REQUEST=$(curl <http://localhost/health>) # blue로 request
if [ -n "$REQUEST" ]
then # 서비스 가능하면 health check 중지
echo "health check success"
break ;
else
sleep 10
fi
done;
if [ $cnt -eq 10 ]
then
echo "서버가 정상적으로 구동되지 않았습니다."
exit 1
fi
echo "4. reload nginx"
sudo cp /home/ubuntu/nginx/config/nginx_blue.conf /home/ubuntu/nginx/config/nginx.conf
sudo docker compose exec nginx service nginx reload
echo "5. green container down"
sudo docker compose down be-green
fi
/github/workflows/main.yml
name: CI/CD using github actions & docker
# event trigger
on:
push:
branches: [ "master" ]
permissions:
contents: read
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
# JDK setting
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
# send docker-compose.yml to be1
- name: Send docker-compose.yml to BE1
uses: appleboy/scp-action@master
with:
key: ${{ secrets.BE1_PRIVATE_KEY }}
host: ${{ secrets.BE1_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./docker-compose.yml"
target: "/home/ubuntu/"
# send docker-compose.yml to be2
- name: Send docker-compose.yml to BE2
uses: appleboy/scp-action@master
with:
key: ${{ secrets.BE2_PRIVATE_KEY }}
host: ${{ secrets.BE2_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./docker-compose.yml"
target: "/home/ubuntu/"
# send deploy.sh be1
- name: Send deploy.sh to BE1
uses: appleboy/scp-action@master
with:
key: ${{ secrets.BE1_PRIVATE_KEY }}
host: ${{ secrets.BE1_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./deploy.sh"
target: "/home/ubuntu/"
# send deploy.sh be2
- name: Send deploy.sh to BE2
uses: appleboy/scp-action@master
with:
key: ${{ secrets.BE2_PRIVATE_KEY }}
host: ${{ secrets.BE2_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./deploy.sh"
target: "/home/ubuntu/"
# docker build & push to hub
- name: Docker build & push to hub
if: contains(github.ref, 'master')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_TOKEN }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/gyunpang-be .
docker push ${{ secrets.DOCKER_USERNAME }}/gyunpang-be
# deploy to be1
- name: run script at BE1
uses: appleboy/ssh-action@master
id: run-script-be1
if: contains(github.ref, 'master')
with:
key: ${{ secrets.BE1_PRIVATE_KEY }}
host: ${{ secrets.BE1_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-be
chmod 777 ./deploy.sh
./deploy.sh
sudo docker image prune -f
## deploy to be2
- name: run script at BE2
uses: appleboy/ssh-action@master
id: run-script-be2
if: contains(github.ref, 'master')
with:
key: ${{ secrets.BE2_PRIVATE_KEY }}
host: ${{ secrets.BE2_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-be
chmod 777 ./deploy.sh
./deploy.sh
sudo docker image prune -f
be1 서버에도 동일한 nginx 설정을 해준 뒤 위와같이 workflow를 작성하면 동일 이미지를 be1,be2에 모두 배포할 수 있게 된다 !
끝 !
참고
https://jojaeng2.tistory.com/86
'Gyunpang' 카테고리의 다른 글
7. Gateway 로깅 및 인증 기능 구현하기(1) (1) | 2024.07.23 |
---|---|
(번외) docker container scale 조정 시 github action에서만 recreate된다.. (0) | 2024.04.13 |
5. Nginx + React 무중단 배포하기 (0) | 2024.03.26 |
4. Docker + Github Action + nginx로 CI/CD 파이프라인 구축하기 (1) | 2024.03.24 |
3. 도메인 연결 및 nginx 설정하기 (0) | 2024.03.24 |