최종적으로 FE,BE,Gateway, 모니터링 서버 등등 모두 무중단 배포를 하기위해서
블루/그린 전략을 선택했다.
선택의 이유는, 인턴을 하며 경험했던 Jenkins와 k8s기반으로 구성된 자동화 시스템의 경우 롤링 업데이트 방식으로 무중단 배포를 했었기도 하고, 롤링 업데이트 형식으로 하려면 여러 pod을 오토스케일링하는 인프라도 추가로 구축이 필요하기에 시간적 소모도 더 들 것이라 판단되어 블루,그린 방식으로 구현하기로 했다.
Blue-Green vs Rolling
블루-그린
- 두 개의 별도 컨테이너(환경)을 사용해서 애플리케이션을 업데이트하는 방식
- 새로운 업데이트를 반영한 애플리케이션을 그린에 배포 → 기존 웹서버, Gateway등의 설정을 변경하여 블루가 아닌 그린으로 트래픽을 몰도록 함 → 그린 환경에서 서비스 운영
- 추후 업데이트 반대로 진행
- 장점
- 롤백이 쉽다
- 테스트,검증 시간을 충분히 확보할 수 있다
- 단점
- 인프라 소스가 두 배 필요하다
- 배포 시간이 길다
Rolling
- 기존 실행중이던 pod(container)를 하나씩 새로운 버전의 pod으로 교체하는 방식
- 새로운 애플리케이션을 배포 → LB설정 변경 → 이전 버전의 pod을 하나씩 새로운 버전의 pod으로 교체하며 서비스 제공 → 모든 pod이 업데이트 완료
- 장점
- 인프라 리소스가 적게든다
- 배포 시간이 빠르다
- 단점
- 롤백이 복잡하다
Single Container 파이프라인 구축하기
무중단 배포를 하기 전에 우선, master 브랜치에 올라간 코드를 감지해서 CI/CD 해주는 파이프라인을 구축해보자. (일반적인 서비스라면 dev,release, product 등의 개발 단계로 분류해서 계층을 나눠야 하지만.. 시간관계상 pass)
전체적인 flow는
- master 브랜치에 push를 github action으로 감지
- jdk세팅 및 gradle캐싱설정
- (yml 설정)
- gradle build
- 도커 빌드 및 hub에 Push
- ssh연결로 인스턴스에서 업로드한 도커 이미지 Pull 및 도커 Run
이다.
먼저 도커를 이용해서 배포할 계획이니 먼저 도커파일을 작성하자
FROM amazoncorretto:17-al2-jdk AS builder
LABEL authors="sok5188"
RUN mkdir /gateway
WORKDIR /gateway
COPY . .
FROM amazoncorretto:17.0.10-alpine
ENV TZ=Asia/Seoul
RUN mkdir /gateway
WORKDIR /gateway
COPY --from=builder /gateway/build/libs/*.jar app.jar
CMD ["java", "-jar", "app.jar"]
다음으론 github action workflow를 작성해보자
# github repository actions 페이지에 나타날 이름
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-
# application.yml
# - name: make application.yml
# if: |
# contains(github.ref, 'master')
# run: |
# mkdir ./src/main/resources # resources 폴더 생성
# cd ./src/main/resources # resources 폴더로 이동
# touch ./application.yml # application.yml 생성
# echo "${{ secrets.YML }}" > ./application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
# shell: bash
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
# 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-gateway .
docker push ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
## deploy to develop
- name: Deploy docker
uses: appleboy/ssh-action@master
id: deploy-docker
if: contains(github.ref, 'master')
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
sudo docker image prune -f
각 Secret이 의미하는 바는 다음과 같다.
- DOCKER_USERNAME : docker hub ID
- DOCKER_TOKEN : docker hub login token (발급받은 access token or PW)
- SSH_PRIVATE_KEY : 배포하려는 인스턴스의 private key (aws ec2의 경우 *.pem으로 저장한 파일을 열어 ===begin ~ end=== 모두 복사해서 넣어주면 된다)
- SSH_HOST : 인스턴스의 주소 (IPv4 or IPv4 DNS)
- SSH_USERNAME : 인스턴스에서 사용할 사용자이름 (ex. ubuntu / 다른 사용자 계정을 사용하고 있다면 해당 사용자 계정을 입력하면된다)
빌드 시 ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey] 이런 오류가 뜬다면 /etc/ssh/sshd_config 파일을 열어
CASignatureAlgorithms +ssh-rsa
위 코드를 추가해준다.
그래도 안되는 경우
PubkeyAcceptedKeyTypes=+ssh-rsa
위 코드도 추가해보자
(나는 해당 오류가 계속 발생해서 원인을 몰라 둘 다 추가했으나, Github Secret에 SSH_USERNAME을 넣지 않았었다..)
이제 해당 파일을 감지 대상 브랜치(master)에 추가하면 자동으로 workflow가 실행되고 도커 컨테이너가 올라가게 된다.
nginx와 workflow를 수정해서 Blue/Green 배포 구현
동일 이미지를 가진 두 컨테이너를 실행할 예정이니 docker-compose를 먼저 구현하자.
sudo apt-get install docker-compose
위 명령을 통해 docker-compose 를 먼저 설치해주고
/home/ubuntu 경로에 아래와 같은 docker-compose.yml 파일을 만들어줬다
version: '3.8'
services:
gateway-blue:
image: sok5188/gyunpang-gateway:latest
container_name: gateway-blue
environment:
CONTAINER_COLOR: "blue"
ports:
- "8080:8080"
gateway-green:
image: sok5188/gyunpang-gateway:latest
container_name: gateway-green
environment:
CONTAINER_COLOR: "green"
ports:
- "8081:8080"
environment 부분에 현재 떠있는 컨테이너를 구분하기 위해 환경변수로 CONTAINER_COLOR를 전달한다.
여기서 조금 주의할 부분이 CONTAINER_COLOR:green / “CONTAINER_COLOR: green” 이런식으로 작성하면 오류가 발생한다.
다음으론, 실행할 배포 스크립트 파일을 생성한다.
deploy.sh
#!/bin/bash
IS_GREEN=$(docker ps | grep green) # 현재 실행중인 App이 blue인지 확인합니다.
if [ -z $IS_GREEN ];then # blue라면
echo "### BLUE => GREEN ###"
echo "1. get green image"
docker compose pull gateway-green # green으로 이미지를 내려받습니다.
echo "2. green container up"
docker compose up -d gateway-green # green 컨테이너 실행
for cnt in {1..10}
do
echo "3. green health check..."
echo "서버 응답 확인중(${cnt}/10)";
REQUEST=$(curl http://127.0.0.1:8081) # green으로 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 /etc/nginx/conf.d/gateway-green-url /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "5. blue container down"
docker compose stop gateway-blue
else
echo "### GREEN => BLUE ###"
echo "1. get blue image"
docker compose pull gateway-blue
echo "2. blue container up"
docker compose up -d gateway-blue
for cnt in {1..10}
do
echo "3. blue health check..."
echo "서버 응답 확인중(${cnt}/10)";
REQUEST=$(curl http://127.0.0.1:8080) # 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 /etc/nginx/conf.d/gateway-blue-url /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "5. green container down"
docker compose stop gateway-green
fi
주석에 설명되어 있지만 정리해보자면
- 실행중인 컨테이너가 무슨 색인지 확인하고
- 현재 실행중인 색이 아닌 색의 컨테이너에 이미지를 내려받고
- 컨테이너를 띄운다
- 변경된 컨테이너로 요청을 보내고 응답이 올 때까지 기다린다 (timeout존재)
- 응답이 정상적으로 온 경우 /etc/nginx/conf.d/gateway-(color)-url 의 파일을 복사하여 /etc/nginx/conf.d/service-url.inc 파일로 덮어쓴다
- nginx를 reload한다.
사실 여기서 계속 KeyError: 'ContainerConfig 라는 오류가 발생했었다.
실제 인스턴스에 터미널로 접속해서 직접 컨테이너를 Up하는 경우에도 똑같은 오류가 발생했었다 ..
그렇게 계속 이유를 찾다 보니.. 여기 에서 그 답을 찾았다.
얼마전부턴 docker-compose가 deprecated 되었고 새로운 도커 API를 위해서 docker compose를 사용해야 한다고 한다.. (………)
위의 과정을 거쳐 컨테이너가 업데이트 되는 것을 알았으니 이제 nginx 설정을 변경해보자
/etc/nginx/sites-available/default 파일에서 api.sirong.shop 도메인 설정을 아래와 같이 수정했다
server {
include /etc/nginx/conf.d/service-url.inc;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name api.sirong.shop; # managed by Certbot
location / {
proxy_pass $service_url;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/api.sirong.shop/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/api.sirong.shop/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
}
살펴볼 부분은 맨 위쪽의 include /etc/nginx/conf.d/service-url.inc; 이다.
해당 경로의 파일을 포함시켜 해당 파일에 적힌 변수를 사용하여 proxy로 전달할 주소를 설정한다.
set $service_url <http://127.0.0.1:8081>;
해당 파일은 위와 같이 변수를 설정할 수 있다.
즉, 이 파일의 값을 8080포트(blue)와 8081포트(green)으로 번걸아가며 덮어씌우고 이를 reload하여 라우팅을 조정하는 것이다.
마지막으로 workflow를 수정하기 전 정상 동작을 확인하기 위한 api를 만들자
@Controller
public class tmpController {
@Autowired
private Environment environment;
@RequestMapping("/health")
public ResponseEntity<String> healthCheck(){
return ResponseEntity.ok("alive");
}
@RequestMapping("/color")
public ResponseEntity<String> colorCheck(){
if(environment.containsProperty("CONTAINER_COLOR")){
String customEnv = environment.getProperty("CONTAINER_COLOR");
return ResponseEntity.ok(customEnv);
}
else return ResponseEntity.ok("NoCOLOR");
}
}
이제 마지막으로 workflow파일을 수정해보자
우리가 필요한 부분은
- docker-compose.yml을 인스턴스에 복사하는 것
- 작성했던 배포 스크립트 deploy.sh를 인스턴스에 복사하는 것
- 기존에 직접 띄우던 방식이 아닌 docker-compose를 통해 띄우기 위해 deploy.sh를 실행하게끔 변경하는 것
위 3가지가 필요하다.
각각의 코드는 아래와 같다.
# send docker-compose.yml
- name: Send docker-compose.yml
uses: appleboy/scp-action@master
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./docker-compose.yml"
target: "/home/ubuntu/"
# deploy.sh 파일 서버로 전달하기(복사 후 붙여넣기)
- name: Send deploy.sh
uses: appleboy/scp-action@master
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./deploy.sh"
target: "/home/ubuntu/"
## deploy
- name: Deploy docker
uses: appleboy/ssh-action@master
id: deploy-docker
if: contains(github.ref, 'master')
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
chmod 777 ./deploy.sh
./deploy.sh
sudo docker image prune -f
전체 workflow는 아래와 같다
# github repository actions 페이지에 나타날 이름
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 - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- 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-
# # 환경별 yml 파일 생성(1) - application.yml
# - name: make application.yml
# if: |
# contains(github.ref, 'master') ||
# contains(github.ref, 'develop')
# run: |
# mkdir ./src/main/resources # resources 폴더 생성
# cd ./src/main/resources # resources 폴더로 이동
# touch ./application.yml # application.yml 생성
# echo "${{ secrets.YML }}" > ./application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
# shell: bash
# # 환경별 yml 파일 생성(2) - dev
# - name: make application-dev.yml
# if: contains(github.ref, 'develop')
# run: |
# cd ./src/main/resources
# touch ./application-dev.yml
# echo "${{ secrets.YML_DEV }}" > ./application-dev.yml
# shell: bash
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
# send docker-compose.yml
- name: Send docker-compose.yml
uses: appleboy/scp-action@master
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./docker-compose.yml"
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-gateway .
docker push ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
# deploy.sh 파일 서버로 전달하기(복사 후 붙여넣기)
- name: Send deploy.sh
uses: appleboy/scp-action@master
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
source: "./deploy.sh"
target: "/home/ubuntu/"
## deploy to develop
- name: Deploy docker
uses: appleboy/ssh-action@master
id: deploy-docker
if: contains(github.ref, 'master')
with:
key: ${{ secrets.SSH_PRIVATE_KEY }}
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: 22
script: |
sudo docker ps
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/gyunpang-gateway
chmod 777 ./deploy.sh
./deploy.sh
sudo docker image prune -f
이제 코드를 master에 올려 정상적으로 동작하는 지 확인해보면
완성 !
참고
https://docs.docker.com/engine/install/ubuntu/
https://velog.io/@leeeeeyeon/Github-Actions과-Docker을-활용한-CICD-구축
https://askubuntu.com/questions/46424/how-do-i-add-ssh-keys-to-authorized-keys-file
https://github.com/appleboy/ssh-action?tab=readme-ov-file
https://velog.io/@leeeeeyeon/Github-Actions과-Docker을-활용한-CICD-구축#-트러블-슈팅-5
https://mr-popo.tistory.com/230
https://engineerinsight.tistory.com/266
https://velog.io/@jungseo/도커-컨테이너에서-환경변수-사용하기
https://devopsnet.tistory.com/42
https://github.com/docker/compose/issues/6511
'Gyunpang' 카테고리의 다른 글
6. 여러대의 인스턴스에 무중단 배포하기 (0) | 2024.04.06 |
---|---|
5. Nginx + React 무중단 배포하기 (0) | 2024.03.26 |
3. 도메인 연결 및 nginx 설정하기 (0) | 2024.03.24 |
2. Docker를 통해 Sonarqube를 프로젝트와 연동시켜보자 (0) | 2024.03.24 |
1. Spring Cloud Gateway를 통해 Simple Gateway를 만들어보자 (0) | 2024.03.24 |