전체 아키텍쳐는 위와 같습니다.
※ Github Actions CI + CodeDeploy로 CI/CD 구현하기에 이어서 진행하겠습니다.
AWS EC2 인스턴스의 리눅스 환경에서 설치하도록 하겠습니다.
sudo amazon-linux-extras install nginx1
설치가 완료되면 sudo service nginx start
으로 서비스를 실행합니다.
이 명령어로 service nginx.status status
해당 서비스가 잘 실행중인지 알 수 있습니다.
그리고 http://(탄력적-ip):80 로 접속하면 아래와 같이 Nginx의 기본화면을 볼 수 있습니다.
(이때, Nginx 는 80포트를 이용하기 때문에 EC2 인스턴스 보안 그룹에서 80을 열어둬야 합니다.)
이제 본격적으로 설정에 들어가보겠습니다.
Nginx의 설정파일인 nginx.conf
파일에 진입합니다.
sudo vim /etc/nginx/nginx.conf
아래와 같이 수정해줍니다.
추가되는 코드는 아래와 같습니다.
include /etc/nginx/conf.d/service-url.inc;
location / {
proxy_pass $service_url;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
}
위에서 include 했던 파일을 생성하겠습니다.
아래의 명령어로 해당 디렉토리에 파일을 생성한 뒤 해당 코드를 넣고 저장한뒤 닫습니다.
sudo vim /etc/nginx/conf.d/service-url.inc
위의 설정을 완료하고 sudo service nginx restart
으로 Nginx를 재시작해줍니다.
이제 Nginx의 설정은 끝입니다!
위의 아키텍쳐를 보시면 8081포트와 8082포트를 넘나드는것을 볼 수 있습니다. 현재 웹 프로젝트가 어떤 포트를 사용하고 있는지 알 수 있게 스프링 프로젝트로 넘어가서 설정해 주도록 하겠습니다.
먼저 application.yml
에서 다음의 코드를 추가합니다.
spring:
profiles:
group:
set1: set1
set2: set2
스프링의 profile 그룹을 두 개 생성하였습니다.
그리고 같은 디렉토리에 application-set1.yml
과 application-set2.yml
를 다음과 같이 생성합니다.
application-set1.yml
spring:
config:
activate:
on-profile: set1
server:
port: 8081
application-set2.yml
spring:
config:
activate:
on-profile: set2
server:
port: 8082
이렇게 하면 기존의 application.yml
에는 set1 set2의 profile 설정의 공통 설정이 들어가며, set1을 실행하면 8081 포트로, set2로 실행하면 8082 포트로 실행됩니다.
해당 profile로 실행하는 방법은 아래와 같습니다.
{
"configurations": [
{
...
"args": [
"--spring.profiles.active=set1"
]
}
]
}
java -jar -Dspring.profiles.active=set1 ./JAR파일이름.jar
이제 어떤 profile로 실행되고 있는지 간단하게 확인할 수 있는 controller를 생성하겠습니다.
package com.example.demo.controller;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.core.env.Environment;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class ProfileController {
private final Environment env;
// 실행중인 프로젝트의 Profile이 뭔지 확인할 수 있는 API
@GetMapping("/profile")
public String getProfile() {
List<String> profile = Arrays.asList(env.getActiveProfiles());
List<String> realProfiles = Arrays.asList("set1", "set2");
String defaultProfile = profile.isEmpty() ? "default" : profile.get(0);
return profile.stream()
.filter(realProfiles::contains)
.findAny()
.orElse(defaultProfile);
}
}
위와 같이 설정하고 /profile
로 진입하면 사용중인 해당 profile이 뜨게됩니다.
이제 스프링 프로젝트의 스크립트 파일과 appspec.yml
파일을 설정하겠습니다.
먼저 appspec.yml
파일을 설정하겠습니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/app/step3/zip/
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ec2-user
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ec2-user
ValidateService:
- location: scripts/health.sh
timeout: 60
runas: ec2-user
스크립트 파일을 이제 하나씩 생성해 보도록 하겠습니다.
#!/usr/bin/env bash
# 쉬고 있는 profile 찾기
# set1이 사용중이면 set2가 쉬고 있으며, 반대편 set1이 쉬고 있다.
function find_idle_profile()
{
RESPONSE_CODE=$(sudo curl -s -o /dev/null -w "%{http_code}" http://localhost/profile)
if [ ${RESPONSE_CODE} -ge 400 ] # 400 보다 크면 (즉, 40x/50x 에러 모두 포함)
then
CURRENT_PROFILE=set2
else
CURRENT_PROFILE=$(sudo curl -s http://localhost/profile)
fi
if [ ${CURRENT_PROFILE} == set1 ]
then
IDLE_PROFILE=set2 # Nginx와 연결되지 않은 profile
else
IDLE_PROFILE=set1
fi
# bash script는 return 기능이 없기 떄문에,
# echo를 통해서 출력하면 이 값을 클라이언트가 사용할 수 있습니다.
echo "${IDLE_PROFILE}"
}
# 쉬고 있는 profile의 port 찾기
function find_idle_port()
{
IDLE_PROFILE=$(find_idle_profile)
if [ ${IDLE_PROFILE} == set1 ]
then
echo "8081" # 여기도 마찬가지로 return 기능의 느낌
else
echo "8082"
fi
}
#!/usr/bin/env bash
# 기존 Eginx엔스에 연결되어 있지 않지만, 실행 중이던 스프링 부트 종료
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH) # 현재 stop.sh가 속해있는 경로
source ${ABSDIR}/profile.sh # 해당 코드로 profile.sh 내의 함수 사용
IDLE_PORT=$(find_idle_port)
echo ">>> $IDLE_PORT 에서 구동중인 애플리케이션 PID 확인"
IDLE_PID=$(sudo lsof -ti tcp:${IDLE_PORT})
if [ -z ${IDLE_PID} ]
then
echo ">>> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
# # Nginx에 연결되어 있지는 않지만 현재 실행 중인 jar 를 Kill 합니다.
echo ">>> kill -15 $IDLE_PID"
kill -15 ${IDLE_PID}
sleep 5
fi
#!/usr/bin/env bash
# 배포할 신규 버전 프로젝트를 stop.sh로 종료한 profile로 실행
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh # 해당 코드로 profile.sh 내의 함수 사용
REPOSITORY=/home/ec2-user/app/step3
echo ">>> Build 파일 복사"
echo ">>> cp $REPOSITORY/zip/build/libs/*.jar $REPOSITORY/"
cp $REPOSITORY/zip/build/libs/*.jar $REPOSITORY/
echo ">>> 새 어플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1) # jar 이름 꺼내오기
echo ">>> JAR Name: $JAR_NAME"
echo ">>> $JAR_NAME 에 실행 권한 추가"
chmod +x $JAR_NAME
echo ">>> $JAR_NAME 실행"
IDLE_PROFILE=$(find_idle_profile)
# 위에서 보았던 것처럼 $IDLE_PROFILE에는 set1 or set2가 반환되는데
# 반환되는 properties를 실행한다는 뜻.
echo ">>> $JAR_NAME 를 profile=$IDLE_PROFILE 로 실행합니다."
nohup java -jar -Dspring.profiles.active=$IDLE_PROFILE $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
#!/usr/bin/env bash
# Ngnix가 바라보는 스프링 부트를 최신 버전으로 변경
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
function switch_proxy() {
IDLE_PORT=$(find_idle_port)
echo ">>> 전환할 Port: $IDLE_PORT"
echo ">>> Port 전환"
# 아래 줄은 echo를 통해서 나온 결과를 | 파이프라인을 통해서 service-url.inc에 덮어쓸 수 있습니다.
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
echo ">>> Reload Nginx"
sudo service nginx reload # Nginx reload를 합니다.
}
#!/usr/bin/env bash
# start.sh로 실행시킨 프로젝트가 정상적으로 실행됐는지 체크
ABSPATH=$(readlink -f $0)
ABSDIR=$(dirname $ABSPATH)
source ${ABSDIR}/profile.sh
source ${ABSDIR}/switch.sh
IDLE_PORT=$(find_idle_port)
echo ">>> Health Check Start!"
echo ">>> IDLE_PORT: $IDLE_PORT"
echo ">>> curl -s http://localhost:$IDLE_PORT/profile"
sleep 10
# for 문 10번 돌기
for RETRY_COUNT in {1..10}
do
# 현재 문제 없이 잘 실행되고 있는 요청을 보내봅니다.
RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
# 해당 결과의 줄 수를 숫자로 리턴합니다.
UP_COUNT=$(echo ${RESPONSE} | grep 'set' | wc -l)
if [ ${UP_COUNT} -ge 1 ]
then # $up_count >= 1 ("set" 문자열이 있는지 검증)
echo ">>> Health check 성공"
switch_proxy # switch.sh 실행
break
else
echo ">>> Health check의 응답을 알 수 없거나 혹은 실행 상태가 아닙니다."
echo ">>> Health check: ${RESPONSE}"
fi
if [ ${RETRY_COUNT} -eq 10 ]
then
echo ">>> Health check 실패. "
echo ">>> 엔진엑스에 연결하지 않고 배포를 종료합니다."
exit 1
fi
echo ">>> Health check 연결 실패. 재시도..."
sleep 10
done
이제 배포 테스트를 해보겠습니다.
CodeDeploy Agent는 아래의 로그파일에 로그가 생성되는데 아래의 명령어로 배포 과정을 확인할 수 있습니다.
tail -f /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
로그도 문제 없고 profile도 성공적으로 바뀐것을 알 수 있습니다!