Information Security Study
240718 private 인스턴스 bastion로 접속, db 설정, 접속 성공, 아틸러리 테스트(부하 분산 확인), 롤백 확인, 보안그룹 분리, 무중단 배포 본문
240718 private 인스턴스 bastion로 접속, db 설정, 접속 성공, 아틸러리 테스트(부하 분산 확인), 롤백 확인, 보안그룹 분리, 무중단 배포
gayeon_ 2024. 7. 18. 19:58private 인스턴스 bastion로 접속
private subnet에 있는 인스턴스들은 bastion으로 ssh 접속이 가능하다.
(오늘 먼저 할 것)
배스천 설정
젠킨스 서버에 키 복사
젠킨스에서 app01로 접속
db01에도 접속 성공
젠킨스 키 발급
공개키를 app01에 저장한다.
젠킨스 비밀키 저장
app01 정보 입력
크리덴셜 설정
pipeline {
tools {
gradle "GRADLE" // Jenkins에서 설정한 Gradle의 이름
}
agent any
stages {
stage('Clone') {
steps {
git branch: 'master', url: 'https://github.com/gayeonni/swu_prj_application.git'
}
}
stage('Build') {
steps {
sh 'pwd'
sh 'ls'
sh 'chmod +x ./gradlew'
sh './gradlew clean build'
sh 'ls ./build/libs'
}
}
}
}
우선 빌드가 되나 테스트해 봤다.
빌드 성공
DB 설정
db01 root 비밀번호 설정(mysql)
접속 시 비밀번호 입력
로컬이 아닌 외부(DB 인스턴스)에 연결할 수 있는 유저 생성
유저를 생성하고 spring_security_inclass 데이터베이스를 생성해야 된다.
해당 db에 모든 권한 부여
보안규칙 추가
생성한 유저로 접속
접속 성공
app01 배포 코드
pipeline {
tools {
gradle "GRADLE" // Jenkins에서 설정한 Gradle의 이름
}
agent any
stages {
stage('Clone') {
steps {
git branch: 'master', url: 'https://github.com/gayeonni/swu_prj_application.git'
}
}
stage('Set Permissions') {
steps {
sh 'chmod +x ./gradlew'
}
}
stage('Test') {
steps {
script {
sh './gradlew test'
}
}
}
stage('Build') {
steps {
sh './gradlew clean build'
sh './gradlew clean build -Pdb.url=jdbc:mysql://db01의ip:3306/spring_security_inclass?useSSL=false -Pdb.username=root -Pdb.password=mysql -Pdb.driver=com.mysql.cj.jdbc.Driver'
// 빌드 후 JAR 파일을 원격 서버로 복사
script {
copyJarToRemote('app01의 ip')
}
}
}
stage('Deploy1') {
steps {
script {
deployAndTest('app01의 ip')
}
}
}
}
post {
success {
echo "This will run when the run finishes successfully"
}
failure {
echo "This will run if failed"
}
}
}
def copyJarToRemote(targetServerIp) {
def jarFile = 'jar 경로'
def deployPath = '/home/ubuntu'
sshagent(['JENKINS-KEY']) {
// 새로운 JAR 파일 전송
def scpCmd = "scp -o StrictHostKeyChecking=no $jarFile ubuntu@$targetServerIp:$deployPath/"
def scpResult = sh(script: scpCmd, returnStatus: true)
if (scpResult != 0) {
echo "Failed to copy jar file to $targetServerIp"
error "Failed to copy jar file"
} else {
echo "Successfully copied jar file to $targetServerIp"
}
}
}
def deployAndTest(targetServerIp) {
def jarFile = 'jar 경로'
def deployPath = '/home/ubuntu'
def runAppCommand = "nohup java -jar $jarFile > $deployPath/log.log 2>&1 &"
def checkLogCommand = "grep -q \"Started\" $deployPath/log.log"
def maxAttempts = 40
def sleepInterval = 3 // 3 seconds
def backupJarFile = 'inclass-spring-security-0.0.1-SNAPSHOT-backup.jar'
sshagent(['JENKINS-KEY']) {
sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'pgrep -f inclass-spring-security-0.0.1-SNAPSHOT.jar && pkill -f inclass-spring-security-0.0.1-SNAPSHOT.jar || echo \"No process found\"'", returnStatus: true
// 백업 파일 생성
def checkBackupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '[ ! -f $deployPath/$backupJarFile ]'"
def checkBackupResult = sh(script: checkBackupCmd, returnStatus: true)
if (checkBackupResult == 0) {
def backupCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'cp $deployPath/inclass-spring-security-0.0.1-SNAPSHOT.jar $deployPath/$backupJarFile'"
def backupResult = sh(script: backupCmd, returnStatus: true)
if (backupResult != 0) {
echo "Failed to create backup file on $targetServerIp"
error "Failed to create backup file"
}
} else {
echo "Backup file already exists on $targetServerIp"
}
// 애플리케이션 실행
def runCmd = "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"
def runResult = sh(script: runCmd, returnStatus: true)
if (runResult != 0) {
echo "Failed to start application on $targetServerIp"
error "Failed to start application"
}
def attempts = 0
def deploymentSuccess = false
while (attempts < maxAttempts) {
int result = sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$checkLogCommand'", returnStatus: true
if (result == 0) {
echo "Deployment to $targetServerIp was successful."
deploymentSuccess = true
break
}
attempts++
sleep sleepInterval
}
if (!deploymentSuccess) {
echo "Deployment to $targetServerIp failed. Rolling back to previous version."
rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
error "Deployment to $targetServerIp failed."
}
// CD 테스트
def cdTestResult = sh script: "curl -s -o /dev/null -w '%{http_code}' http://$targetServerIp:8080", returnStdout: true
echo "cdTestResult: '${cdTestResult.trim()}'"
if (cdTestResult.trim() != "200") {
echo "CD test failed for $targetServerIp. Rolling back to previous version."
rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand)
} else {
echo "CD test passed for $targetServerIp."
}
}
}
def rollbackToPreviousVersion(targetServerIp, backupJarFile, deployPath, runAppCommand, checkLogCommand) {
def maxAttempts = 40
def sleepInterval = 3 // 3 seconds
sshagent(['JENKINS-KEY']) {
// 현재 실행 중인 애플리케이션 프로세스 종료
sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'pgrep -f inclass-spring-security-0.0.1-SNAPSHOT.jar && pkill -f inclass-spring-security-0.0.1-SNAPSHOT.jar || echo \"No process found\"'", returnStatus: true
// 백업 파일을 원본 파일로 덮어씌우기
sh script: """
ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'if [ -f $deployPath/$backupJarFile ]; then
mv -f $deployPath/$backupJarFile $deployPath/inclass-spring-security-0.0.1-SNAPSHOT.jar && echo "Backup file moved successfully."
else
echo "Backup file does not exist."
fi'
""", returnStatus: true
// 원본 파일에 실행 권한 설정
sh script: """
ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp 'chmod +x $deployPath/inclass-spring-security-0.0.1-SNAPSHOT.jar && echo "Execution permission set on original file."'
""", returnStatus: true
// 애플리케이션 재실행
sh "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$runAppCommand'"
// 롤백 완료 여부 확인
def attempts = 0
def rollbackSuccess = false
while (attempts < maxAttempts) {
int result = sh script: "ssh -o StrictHostKeyChecking=no ubuntu@$targetServerIp '$checkLogCommand'", returnStatus: true
if (result == 0) {
echo "Rollback to previous version on $targetServerIp was successful."
rollbackSuccess = true
break
}
attempts++
sleep sleepInterval
}
if (!rollbackSuccess) {
error "Rollback to previous version on $targetServerIp failed."
}
}
}
빌드하면...
nginx로 애플리케이션 접속 완료!!
db와 app 모두 동작합니다. ㅎㅎ
아틸러리 테스트(부하 분산 확인)
부하 분산
: 한 가용영역 안에서 트래픽 분산을 위해 동일한 여러 app or 두 서버를 사용하는 것
서버 이중화
: 한 서버에 장애가 발생해도 다른 서버로 서비스를 유지할 수 있도록 두 서버를 두는 것
현재 가용영역 a에서는 app01이 배포가 된 상태이다.
아틸러리 테스트, 도커 배포 등등 여러 단계를 모두 완료한 후에 문서 참고해서 가용영역 b도 그대로 완성시킬 예정이다. (가용영역 b에는 젠킨스 설치, 크리덴셜 설정까지 완료했다.)
부하분산을 위해 app01을 가용영역 a 안에 하나만 복제할 것이다.
복제하기 위해 app01로 이미지를 만들고 인스턴스를 생성했다.
upstream cpu-bound-app {
server app01의 ip:8080 weight=100 max_fails=3 fail_timeout=3s;
server app01-1의 ip:8080 weight=100 max_fails=3 fail_timeout=3s;
}
그리고 복제한 인스턴스의 private ip를 nginx 설정 파일에 넣어야 한다.
설정파일 재입력
젠킨스 대시보드의 시스템 탭에서도 app01-1를 등록하면 스트레스 테스트 준비 끝!
스트레스 테스트를 할 때는 vscode를 관리자 권한으로 실행해야 한다.
부하를 조절할 때는 웹 애플리케이션에 대한 동시 요청 수인 arrivalRate 값을 변경하면 된다.
config:
target: "http://ip" // nginx01의 공개 ip
phases:
- duration: 30
arrivalRate: 100
name: "warm up"
- duration: 60
arrivalRate: 1500
name: "actual"
scenarios:
- flow:
- get:
url: "/products-temp/thymeleaf/ex01"
스트레스 테스트 vscode
artillery run --output report.json script.yaml
부하테스트 실행 명령어
artillery.cmd report .\report.json
리포트 생성 명령어
1) app01 하나일 때(수직 스케일링)
2) app01, app01-1일 때(수평 스케일링)
중앙값과 95번째 백분위 값이 큰 차이가 나지 않으면 안정적인 서버이다.
수직 스케일링 방법을 사용했을 때보다 중앙값과 95번째 백분위 값의 차이가 덜 난다.
+)
로그에서 균등하게 로드밸런싱이 되고 있는 것을 볼 수 있었다.
보안그룹 분리
지금까지는 실습을 위해 그냥 기본 보안그룹에 모든 규칙을 추가했었지만
이렇게 하면 보안상 특정 서비스나 애플리케이션이 필요로 하지 않는 접근도 허용될 수 있기 때문에
기능별로 그룹을 분리해주었다.
기능별로 보안그룹을 사용하기 위해
app, db, jenkins, nginx의 보안그룹을 생성했다.
젠킨스 브라우저는 8080으로 접속하므로 8080번과 ssh 접속을 위해 22번만 열어두었다.
nginx도 설정파일 변경 등으로 ssh 접속이 필요하기 때문에 22번과 서비스 이용을 위한 80번을 열어두었다.
app도 .jar 파일 확인 등을 위해 22번, 서비스 접속을 위한 8080번을 열었다.
db는 브라우저 접속 등이 필요하지 않기 때문에 3306번만 열어두었다.
추후에 추가로 설정이 필요하다면 22번을 잠깐씩 여는 방식으로 사용할 것이다.
롤백 확인
/users/signup 엔드포인트를 주석처리를 했을 때 롤백이 잘 되는지 확인했다.
주석처리를 하고 commit change를 하면
webhooks 설정으로 인해 push를 자동으로 감지해서 빌드된다.
배포에 실패해 이전 버전으로 롤백한다고 출력되었고
백업파일도 성공적으로 덮어씌워졌다고 출력되었다.
그런데 이상하게 결과는 실패인.. 상황
무중단 배포
롤링 배포
- 애플리케이션의 인스턴스를 순차적으로 업데이트하여 서비스 중단을 최소화하는 방식이다.
- 각 인스턴스를 업데이트하고 새로운 인스턴스가 성공적으로 시작되면 다음 인스턴스를 업데이트하는 방식이다.
이미 기존 코드에 롤링 방식으로 배포하는 구문이 포함되어있어 호출 부분만 살짝 변경했다.
pipeline {
tools {
gradle "GRADLE" // Jenkins에서 설정한 Gradle의 이름
}
agent any
stages {
stage('Clone') {
steps {
git branch: 'master', url: 'https://github.com/gayeonni/swu_prj_application.git'
}
}
stage('Set Permissions') {
steps {
sh 'chmod +x ./gradlew'
}
}
stage('Test') {
steps {
script {
sh './gradlew test'
}
}
}
stage('Build') {
steps {
sh './gradlew clean build'
sh './gradlew clean build -Pdb.url=jdbc:mysql://db의 ip:3306/spring_security_inclass?useSSL=false -Pdb.username=root -Pdb.password=mysql -Pdb.driver=com.mysql.cj.jdbc.Driver'
// 빌드 후 JAR 파일을 원격 서버로 복사
script {
copyJarToRemote('app01의 ip')
copyJarToRemote('app01-1의 ip')
}
}
}
// 롤링 배포 부분
stage('Rolling Deployment') {
steps {
script {
def servers = ['app01의 ip', 'app01-1의 ip']
for (server in servers) {
deployAndTest(server)
}
}
}
}
}
post {
success {
echo "This will run when the run finishes successfully"
}
failure {
echo "This will run if failed"
}
}
}
// 이하 함수는 동일
블루/그린 배포
- 두 개의 동일한 환경(블루와 그린)을 유지한다.
- 하나의 환경에서 새 버전을 배포하고 테스트한 후 문제가 없으면 트래픽을 새 환경으로 전환하는 방식이다.
- 문제가 발생하면 이전 환경으로 바로 롤백할 수 있다.
pipeline {
tools {
gradle "GRADLE" // Jenkins에서 설정한 Gradle의 이름
}
agent any
stages {
stage('Clone') {
steps {
git branch: 'master', url: 'https://github.com/gayeonni/swu_prj_application.git'
}
}
stage('Set Permissions') {
steps {
sh 'chmod +x ./gradlew'
}
}
stage('Test') {
steps {
script {
sh './gradlew test'
}
}
}
stage('Build') {
steps {
sh './gradlew clean build'
sh './gradlew clean build -Pdb.url=jdbc:mysql://db의 ip:3306/spring_security_inclass?useSSL=false -Pdb.username=root -Pdb.password=mysql -Pdb.driver=com.mysql.cj.jdbc.Driver'
// 빌드 후 JAR 파일을 원격 서버로 복사
script {
copyJarToRemote('app01의 ip') // Blue 서버
copyJarToRemote('app01-1의 ip') // Green 서버
}
}
}
stage('Deploy to Green') {
steps {
script {
deployAndTest('app01-1의 ip') // Green 서버에 배포 및 테스트
}
}
}
stage('Switch Traffic to Green') {
steps {
script {
switchTraffic('green')
}
}
}
}
post {
success {
echo "This will run when the run finishes successfully"
}
failure {
echo "This will run if failed"
script {
switchTraffic('blue')
}
}
}
}
// 이하 함수는 동일
// Traffic 전환 함수 추가
def switchTraffic(target) {
if (target == 'green') {
echo "Switching traffic to Green environment"
// 트래픽을 Green 환경으로 전환하는 로직 구현
} else if (target == 'blue') {
echo "Switching traffic to Blue environment"
// 트래픽을 Blue 환경으로 전환하는 로직 구현
}
}
'네트워크 캠퍼스 > 2차 프로젝트' 카테고리의 다른 글
롤백 기능 구현하기 (2) | 2024.07.23 |
---|---|
기본에서 새 vpc로 변경하기 (0) | 2024.07.23 |
보안그룹 분리하기 (0) | 2024.07.18 |
아틸러리로 스트레스 테스트와 부하 분산확인하기 (0) | 2024.07.18 |
애플리케이션 접속 성공 (0) | 2024.07.18 |