이전 프로젝트를 진행했을 때 배포 작업을 수행하는 동안 서버가 다운되는 문제가 있어 이를 해결하고자 Blue/Green 배포를 도입하고자 했다!
이 과정에서 고민한 점은 다음과 같다.
Github Actions VS Jenkins
- Github Actions : Github에서 직접 제공해주는 CI/CD 도구로, Github 저장소에서 발생하는 build, test, package, release, deploy 등 다양한 이벤트를 기반으로 직접 원하는 Workflow를 만들 수 있음.
- Jenkins : 거의 모든 언어의 조합과 소스코드 리포지토리(Repository)에 대한 지속적인 통합(CI)과 지속적인 배포(CD)를 제공함. 다른 일상적인 개발 작업을 자동화할 뿐 아니라 파이프라인(Pipeline)을 사용해 거의 모든 언어의 조합과 소스코드 리포지토리에 대한 지속적인 통합과 지속적인 전달 환경을 구축하도록 도와줌.
Jenkins를 사용하면 별도의 서버를 띄워야 하며 현재 프로젝트 규모가 작기 때문에 상대적으로 가볍고 구축하기 쉬운 Github Actions를 이용하기로 결정했다.
무중단 배포 방식
- Rolling : 새로 배포되어야 하는 버전을 하나씩 순차적으로 적용시키면서 배포하는 방식. 한 번에 모두 배포되는 게 아니기 때문에 배포가 되는 과정에서 옛날 버전과 새로운 버전이 공존해, 잘못하면 배포하는 과정 중에 호환성 문제가 생길 수 있음.
- Blue/Green : Blue 혹은 Green 버전 둘 중 하나로 배포되어 있는 상태에서 새로운 버전을 동시에 띄우고 로드밸런서를 통해서 스위칭하는 방식이며, 한 번에 두 개의 버전을 동시에 띄우기 때문에 시스템 자원이 두배로 든다는 단점이 있음.
- Canary : 카나리 배포는 위험을 빠르게 감지할 수 있는 배포전략으로 지정한 서버 또는 특정 User 에게만 배포해서 서비스를 운영하다가, 버그가 없고 정상적이라고 판단되면 전체에게 배포하는 방식임.
Blue 1대, Green 1대를 구성하므로 자원 소모가 크지 않다는 점과 레퍼런스가 많아 상대적으로 쉽고 구축이 빠르다는 점에 입각해 Blue/Green 배포 방식을 선택했다.
System Architecture
1. EC2
Spring Boot 애플리케이션과 CodeDeploy Agent와 같은 배포를 위한 도구들을 설치할 메인 인스턴스를 하나 만든다.
- AWS에서 EC2 탭에 들어가서 "인스턴스 시작" 클릭
- 인스턴스 설정
2-1 EC2 이름 설정
2-2 애플리케이션 및 OS 이미지 "Ubuntu" 설정
2-3 인스턴스 유형을 프리티어인 t2.micro로 설정
2-4 인스턴스 접속에 필요한 키 페어를 설정- RSA 암호화 방식의 .pem 파일을 사용
- EC2 접속 및 연결 확인
$ ssh -i [pem 키] username@[ip 주소]
2. S3 버킷
Github Actions를 통해 빌드된 파일을 업로드하기 위한 S3 버킷을 하나 만들어야 한다.
- AWS에서 S3 탭에 들어가서 "버킷 생성" 클릭
- 버킷 설정
2-1. 버킷 이름 설정
2-2. 나머지는 모두 default 값으로 두고 "버킷 만들기" 클릭- 퍼블릭 엑세스 차단 설정에서 "모든 퍼블릭 엑세스 차단" 확인
- AWS의 IAM 탭에 들어가서 "사용자 생성" 클릭
- 사용자 설정
4-1. 사용자 이름 설정
4-2. 권한 정책 설정- AWSCodeDeployFullAccess, AmazonS3FullAccess를 선택
- 사용자의 보안 자격증명 메뉴에서 "액세스 키 만들기" 클릭
- 생성된 액세스키와 비밀 액세스키를 보관
3. Code Deploy
3-1. Code Deploy 설정하기
Code Deploy는 소스 코드를 운영환경에 자동 배포하는 역할을 수행하는 AWS Service로 CD, 즉 지속적인 배포 서비스다. CodeDeploy의 배포 대상에는 EC2, ECS, Lambda 등 여러 가지가 존재하지만, EC2에 배포하는 방법을 알아본다.
- AWS에서 IAM 탭에 들어가서 "역할 만들기" 클릭
- 역할 설정
2-1. 일반 사용 사례 "EC2" 선택
2-2. 역할 이름 설정
2-3. 권한 정책 설정- AmazonEC2RoleforAWSCodeDeploy
- 만든 역할을 EC2에서 사용하도록 설정
4. 역할 변경 후 인스턴스 재부팅
5. EC2 접속
$ ssh -i [pem 키] username@[ip 주소]
6. EC2에 CodeDeploy Agent 설치하기
$ sudo apt update
$ sudo apt install ruby-full
$ sudo apt install wget
$ cd /home/ubuntu
$ wget https://bucket-name.s3.region-identifier.amazonaws.com/latest/install
$ chmod +x ./install
$ sudo ./install auto
7. CodeDeploy Agent 상태 확인 (running)
$ sudo service codedeploy-agent status
8. CodeDeploy 서비스에서 사용할 IAM 역할 생성
8-1. 일반 사용 사례 "CodeDeploy" 선택
8-2. 역할 이름 설정
9. AWS에서 CodeDeploy탭에 들어가서 "애플리케이션 생성" 클릭
10. 생성된 애플리케이션을 선택하고 배포 그룹을 생성
11. 환경 구성 - 배포 설정 - CodeDeployDefault.AllAtOnce
3-2. appspec.yml
- appspec.yml 파일은 AWS CodeDeploy의 애플리케이션 스펙 파일로 yaml 또는 Json 형식의 파일
- 배포 프로세스를 정의하고 AWS CodeDeploy가 어떻게 애플리케이션을 배포해야 하는지를 정의
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
ApplicationStart:
- location: deploy.sh
timeout: 60
runas: ubuntu
3-3. deploy.sh
- 배포를 위해 실행되는 파일
- flow (green 버전이 현재 실행 중이라 가정)
- green 버전이 현재 실행 중인지 확인
- green 버전이 현재 실행 중이라면 blue 버전 up
- 30초간 blue, green 버전 동시 실행
- 30초 후 blue 버전 확인
4-1. if blue 버전 실행 O → green 버전 종료
4-2. if blue 버전 실행 X → blue 버전에 런타임 에러 발생했으므로 slack 알람 전송
deploy.sh
# 작업 디렉토리를 /home/ubuntu으로 변경
cd /home/ubuntu
# 환경변수 DOCKER_APP_NAME을 connectdog으로 설정
DOCKER_APP_NAME=connectdog
# 실행 중인 blue가 있는지 확인
# 프로젝트의 실행 중인 컨테이너를 확인하고, 해당 컨테이너가 실행 중인지를 EXIST_BLUE 변수에 저장
EXIST_BLUE=$(sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)
# 배포 시작한 날짜와 시간을 기록
echo "배포 시작 일자 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# green이 실행 중이면 blue up
# EXIST_BLUE 변수가 비어있는지 확인
if [ -z "$EXIST_BLUE" ]; then
# 로그 파일(/home/ubuntu/deploy.log)에 "blue up - blue 배포 : port:8081"이라는 내용을 추가
echo "blue 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# docker-compose.blue.yml 파일을 사용하여 connectdog-blue 프로젝트의 컨테이너를 빌드하고 실행
sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build
# 30초 동안 대기
sleep 30
# blue가 문제 없이 배포 되었는지 현재 실행여부를 확인한다
BLUE_HEALTH=$(sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep Up)
# blue가 현재 실행중이지 않다면 -> 런타임 에러 또는 다른 이유로 배포가 되지 못한 상태
if [ -z "$BLUE_HEALTH" ]; then
# slack으로 알람을 보낼 수 있는 스크립트를 실행한다.
sudo ./slack_blue.sh
# blue가 현재 실행되고 있는 경우에만 green을 종료
else
# /home/ubuntu/deploy.log: 로그 파일에 "green 중단 시작"이라는 내용을 추가
echo "green 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# docker-compose.green.yml 파일을 사용하여 connectdog-green 프로젝트의 컨테이너를 중지
sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
# 사용하지 않는 이미지 삭제
sudo docker image prune -af
echo "green 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
fi
# blue가 실행중이면 green up
else
echo "green 배포 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build
sleep 30
# green이 문제 없이 배포 되었는지 현재 실행여부를 확인한다
GREEN_HEALTH=$(sudo docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml ps | grep Up)
# green이 현재 실행중이지 않다면 -> 런타임 에러 또는 다른 이유로 배포가 되지 못한 상태
if [ -z "$GREEN_HEALTH" ]; then
# slack으로 알람을 보낼 수 있는 스크립트를 실행한다.
sudo ./slack_green.sh
else
# /home/ubuntu/deploy.log: 로그 파일에 "blue 중단 시작"이라는 내용을 추가
echo "blue 중단 시작 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# docker-compose.blue.yml 파일을 사용하여 connectdog-green 프로젝트의 컨테이너를 중지
sudo docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
# 사용하지 않는 이미지 삭제
sudo docker image prune -af
echo "blue 중단 완료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
fi
fi
echo "배포 종료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
echo "===================== 배포 완료 =====================" >> /home/ubuntu/deploy.log
echo >> /home/ubuntu/deploy.log
4. Docker
4-1. EC2에 Docker와 Docker-Compose를 설치
- docker 설치
$ sudo apt update
$ sudo apt install docker.io
2. docker 시작
$ sudo systemctl start docker
3. docker-compose 설치
$ sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
4. docker-compose 권한 부여
$ sudo chmod +x /usr/local/bin/docker-compose
5. docker-compose 권한 확인
$ docker-compose version
4-2. docker-compose.yml
- Docker compose는 컨테이너로부터 이루어진 서비스를 구축, 실행하는 순서를 자동으로 해서, 관리를 간단히 함
- blue, green 무중단 배포를 구성하므로 docker-compose.blue.yml와 docker-compose.green.yml 파일을 따로 작성
docker-compose.blue.yml
#blue
version: '3'
services:
backend:
build: .
ports:
- "8081:8080"
container_name: connectdog-blue
environment:
TZ: "Asia/Seoul"
docker-compose.green.yml
#green
version: '3'
services:
backend:
build: .
ports:
- "8082:8080"
container_name: connectdog-green
environment:
TZ: "Asia/Seoul"
4-3. dockerfile
- dockerfile은 image를 빌드하기 위한 파일이며 이 image를 기반으로 컨테이너가 실행됨
- 아래 코드는 Java 17을 기반으로 하는 애플리케이션을 컨테이너 내에서 실행하기 위한 것으로, 빌드된 애플리케이션 JAR 파일을 Docker 이미지 안에 포함하고 그 JAR 파일을 실행하는 구성을 나타냄
FROM openjdk:17
ARG JAR_FILE=connectdog-0.0.1-SNAPSHOT.jar
COPY $JAR_FILE app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
5. Github Actions
- 프로젝트를 업로드한 Repository의 Actions에 들어가 "java with Gradle"를 선택
- develop 브랜치에 push시 build & deploy가 되도록 함
build workflow
name: connectdog CICD(deploy)
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v3
# JDK를 17 버전으로 세팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# Gradle 캐싱-> 빌드 속도 UP
- 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-jwt.yml 파일 생성
- name: make application-jwt.yml
run: |
cd ./src/main/resources
touch ./application-jwt.yml
echo "${{ secrets.APPLICATION_JWT_YML }}" > ./application-jwt.yml
shell: bash
# application-dev.yml 파일 생성
- name: make application-dev.yml
run: |
cd ./src/main/resources
touch ./application-dev.yml
echo "${{ secrets.APPLICATION_DEV_YML }}" > ./application-dev.yml
shell: bash
# firebase-key.json 생성
- name: create firebase-key.json
run: |
cd ./src/main/resources
touch ./firebase-key.json
echo "${{ secrets.FCM }}" | base64 --decode > ./firebase-key.json
ls -la
shell: bash
# ./gradlew 권한 설정
- name: ./gradlew 권한 설정
run: chmod +x ./gradlew
# Gradle build (Test 제외)
- name: Build with Gradle
run: ./gradlew build -x test
# AWS에 연결
- name: Connect to AWS
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
# 빌드파일을 ZIP 파일로 생성
- name: Make zip file
run: |
mkdir deploy
cp ./docker-compose.blue.yml ./deploy/
cp ./docker-compose.green.yml ./deploy/
cp ./appspec.yml ./deploy/
cp ./Dockerfile ./deploy/
cp ./scripts/deploy.sh ./deploy/
cp ./build/libs/*.jar ./deploy/
zip -r -qq -j ./connectdog-build.zip ./deploy
# S3에 zip 파일 업로드
- name: Upload to S3
run: |
aws s3 cp \
--region ap-northeast-2 \
./connectdog-build.zip s3://connectdog-bucket
# CodeDeploy에 배포 요청
- name: Code Deploy Deployment Request
run: |
aws deploy create-deployment --application-name ConnectDog \
--deployment-config-name CodeDeployDefault.OneAtATime \
--deployment-group-name ConnectDog \
--s3-location bucket=connectdog-bucket,bundleType=zip,key=connectdog-build.zip
6. Nginx
6-1. Nginx 연결하기
Reverse proxy 서버를 위해 Nginx를 설치한다. Reverse proxy의 장점으로는 로드밸런싱, 캐싱, SSL 터미네이션 등이 있다. 나는 Nginx에 SSL 인증서를 설치해서 HTTPS를 적용했고 로드밸런싱 기능도 사용했다. 참고로 배포 시점에 30초간 blue, green 버전이 동시에 띄어져 있는데 이때 짧게나마 로드밸런싱이 적용된다.
ec2에 nginx를 설치하고 연결해보자.
- ec2 접속
$ ssh -i [pem 키] username@[ip 주소]
2. nginx 설치
$ sudo apt install nginx
3. nginx 시작
$ sudo systemctl start nginx
4. 현재 nginx 상태 확인 (초록불 정상)
$ sudo systemctl status nginx
5. nginx 설정 파일 수정
$ sudo vi /etc/nginx/nginx.conf
nginx.conf
# Nginx 이벤트 모듈 설정.
events {}
# HTTP 요청과 관련된 설정을 하는 부분.
http {
# upstream 서버 그룹을 정의. 로드 밸런싱을 위해 여러개의 서버를 가리킬 수 있음.
upstream spring-server {
least_conn;
# 로드 밸런싱을 위해 locahost의 8081 포트와 8082 포트에 동작하는 서버를 지정.
server localhost:8081 max_fails=3 fail_timeout=10s;
server localhost:8082 max_fails=3 fail_timeout=10s;
}
# 실제 HTTP 서버를 설정하는 부분.
server {
# listen 지시문은 서버가 80포트에서 들어오는 요청을 수신하도록 설정.
listen 80;
server_name [my domain name]
include /etc/nginx/default.d/*.conf;
# 모든 경로에 대한 처리를 정의. 프록시 서버로의 요청을 설정하는 부분.
location / {
# proxy_set_header는 프록시 서버로 전달되는 요청 헤더를 설정하는 역할.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header HOST $http_host;
proxy_set_header X-Nginx-Proxy true;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_pass는 실제 요청을 전달할 upstream 서버 그룹을 지정.
proxy_pass http://spring-server;
# proxy_redirect는 프록시 응답의 리다이렉션을 설정하는 부분.
proxy_redirect off;
}
}
}
6-2. Nginx에 SSL 적용
nginx에 SSL 인증서를 발급해 HTTPS를 적용해보자!
CA로는 무료 SSL 인증서 발급 기관인 Let's Encrypt를 사용했다
참고:
Let’s Encrypt 인증서로 NGINX SSL 설정하기
이 모범 사례에서는 Let’s Encrypt 클라이언트를 사용하여 인증서를 생성하는 방법과 이를 사용하도록 NGINX 오픈소스 및 NGINX Plus를 사용하여 nginx ssl 설정을 자동으로 구성하는 방법을 다룹니다.
nginxstore.com
7. Slack
프로젝트에서는 팀원들과 slack을 통해서 협업하고 있어, 배포가 실패할 경우 slack으로 알림을 받도록 하였다.
(blue/green 버전 교체 시 런타임 에러로 서버가 내려가는 경우 -> 런타임 에러 발생)
slack을 통해 알림을 받으려면 Action-Slack을 활용하면 된다.
- Slack API 사이트에 접속한다.
- "create new app"을 누르고 본인이 원하는 App Name, development slack workspace를 선택해서 새로운 앱을 만든다.
- 만든 앱을 선택한 다음 "Incoming webhooks"를 클릭한다.
- Incoming Webhooks를 클릭하여 활성화한다. 그 후 "Add New Webhook to workspace"를 클릭하여 slack에 내가 원하는 채널과 연동시킨다.
- allow를 클릭하면 Webhook URL이 발급된다.
- 알림 설정
- slack_blue.sh
# slack-web-hook URL 세팅
slack_web_hook="........."
# 배포 중 문제가 발생했다는 내용의 로그를 남겨준다.
echo "blue 배포 중 문제 발생 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
echo "관리자 알람 발송 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# 슬랙으로 보낼 메시지를 변수에 저장해준다.
json="{ \"text\": \"blue 배포 중 문제가 발생하여 배포가 비정상 중단되었으니 확>인 부탁드립니다 -> 문제 발생 시각: $(date '+%Y-%m-%d %H:%M:%S')\" }"
# 변수에 메시지가 잘 입력되었는지 콘솔 창에 출력해본다.
echo "json: $json"
# 슬랙으로 메시지를 발송한다.
curl -X POST -H 'Content-type: application/json' --data "$json" "$slack_web_hook"
# 슬랙 알람 발송 이후 배포 비정상종료 로그를 남겨준다.
echo "관리자 알람 발송 완료, 배포 비정상종료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
- slack_green.sh
# slack-web-hook URL 셋팅
slack_web_hook="....."
# 배포 중 문제가 발생했다는 내용의 로그를 남겨준다.
echo "green 배포 중 문제 발생 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
echo "관리자 알람 발송 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
# 슬랙으로 보낼 메시지를 변수에 저장해준다.
json="{ \"text\": \"green 배포 중 문제가 발생하여 배포가 비정상 중단되었으니 확
인 부탁드립니다 -> 문제 발생 시각: $(date '+%Y-%m-%d %H:%M:%S')\" }"
# 변수에 메시지가 잘 입력 되었는지 콘솔 창에 출력해본다.
echo "json: $json"
# 슬랙으로 메시지를 발송한다.
curl -X POST -H 'Content-type: application/json' --data "$json" "$slack_web_hook"
# 슬랙 알람 발송 이후 배포 비정상종료 로그를 남겨준다.
echo "관리자 알람 발송 완료, 배포 비정상종료 : $(date +%Y)-$(date +%m)-$(date +%d) $(date +%H):$(date +%M):$(date +%S)" >> /home/ubuntu/deploy.log
배포를 실패했을 때 슬랙 알림 메시지는 다음과 같다.
참고 블로그:
Github Actions & Nginx를 이용한 CI/CD 무중단 배포 자동화 구축 - EC2 & S3 설정
두 번의 프로젝트를 진행하는 동안, 항상 수동으로 배포를 하곤 했습니다.AWS에 EC2 인스턴스를 만들고, 인스턴스에 접근해서 프로젝트를 빌드한 war 파일을 옮겨서 실행시키는 방법으로 배포를
velog.io
Github Actions +S3 + CodeDeploy + Docker + Nginx + Slack을 활용한 무중단 자동 배포 환경 구축기(with Blue/Green)
Seniors 프로젝트
velog.io
'AWS, CI&CD' 카테고리의 다른 글
Blue-Green 배포 중 트러블 슈팅 (0) | 2023.10.03 |
---|---|
EC2 RDS 연결 (0) | 2023.10.01 |
AWS + Docker + Github Actions를 이용한 CI/CD (0) | 2023.07.25 |