Docker 기반 Web 3‐Tier 아키텍처 구축 보고서 - 100-hours-a-week/2-hertz-wiki GitHub Wiki

📚 목차


📄 Docker 기반 Web 3-Tier 아키텍처 구축 보고서

본 문서는 서비스의 컨테이너화(Containerization)Web 3-Tier 아키텍처 기반 인프라의 구축 현황을 정리한 보고서입니다. 사용자 수 증가 및 서비스 기능 확장에 대응하기 위해, 각 애플리케이션을 독립적으로 운영할 수 있도록 Docker 기반 컨테이너 환경을 도입하였으며, 보안성과 유지보수성 강화를 위해 웹, 애플리케이션, 데이터베이스 계층을 분리한 Web 3-Tier 아키텍처를 설계하고 적용하였습니다. 이를 통해 서비스 안정성 확보, 배포 유연성 향상, 보안 강화 등의 효과를 기대하고 있습니다.

1. 구축 배경 및 목적

서비스 초기에는 단일 서버 환경에서 모든 컴포넌트를 통합하여 운영하였으나, 사용자 수의 증가와 확장에 따라 다음과 같은 한계가 발생했습니다:

  • 회원가입 기능 처리 시 리소스 부족으로 인해 전체 서비스의 안정성이 저하됨
  • 버전 업데이트 시 무중단 배포 미지원으로 인해 서비스 중단 발생
  • 커밋 기반 롤백 방식의 불안정성으로 인해 장애 대응의 신속성과 신뢰성이 부족함

이러한 문제를 해결하고자, 서비스의 각 구성 요소(Frontend, Backend, AI, DB 등)를 Docker 기반 Web 3-Tier 아키텍처로 분리하였습니다. 이를 통해 서비스 간 독립성과 확장성을 확보하고, 안정적인 배포 및 운영 환경을 조성하였습니다.

2. 기술 및 아키텍처 선정 배경

2.1 Web 3-Tier 아키텍처

서비스 구성요소를 Presentation(Frontend), Application(Backend), Data(DB) 계층으로 분리한 Web 3-Tier 아키텍처는 다음과 같은 이유로 채택되었습니다.

  • 보안성 강화

데이터베이스는 퍼블릭 네트워크로부터 격리하고, Backend 계층을 통해서만 통신이 이루어지도록 구성함으로써 데이터 보안성을 높였습니다. 기존에는 DB가 퍼블릭 서브넷에 위치해 외부 노출 위험이 있었으나, 아키텍처 분리를 통해 이러한 위험을 제거했습니다.

  • 확장성 및 고가용성 확보

각 계층은 독립적으로 수평 확장이 가능하여, 특정 계층의 트래픽 급증 또는 리소스 병목이 전체 시스템에 영향을 주지 않도록 하였습니다. 기존 단일서버의 Spring Boot 기반 Backend 서버의 리소스 점유율이 급증하는 상황에서, 해당 계층만 확장함으로써 효율적인 리소스 분배와 고가용성을 실현할 수 있습니다.

  • CI/CD 파이프라인 최적화

계층 분리에 따라 각 서비스는 독립된 배포 단위로 관리되며, 이를 통해 충돌 없는 병렬 배포와 자동화된 운영이 가능해졌습니다. 기존에는 프론트엔드와 백엔드가 하나의 인스턴스에서 동작하면서, 배포 타이밍 충돌로 인한 오류가 빈번하게 발생했으나, 구조 분리 이후 이를 효과적으로 해소하였습니다.

AI 서버의 경우 GPU 서버의 비용 문제와 통일성을 위해 모두 GCP에 두었습니다

2.2 Docker 기반 컨테이너화 필요성

서비스 전반에 Docker 기반의 컨테이너 기술을 도입한 주요 목적은 배포 자동화, 운영 효율성, 환경 일관성 확보 등 다양한 측면에서 기존 운영 환경의 한계를 극복하기 위함입니다. 주요 도입 배경은 다음과 같습니다:

  • 신속한 롤백 및 확장 처리 자동화

기존에는 수동 환경 설정에 의존하여 배포 및 복구 속도가 느리고, 업데이트 발생 시 운영자가 직접 환경을 수정해야 했습니다. Docker 도입 후, 초기 환경을 이미지 단위로 표준화함으로써 배포 및 롤백 자동화가 가능해졌으며, 무중단 롤백 전략을 통해 서비스 가용성과 안정성을 높였습니다. 또한, 컨테이너 기반 확장으로 인해 서비스 확장의 유연성이 크게 향상되었습니다.

  • 배포 추적과 관리 용이

이전에는 커밋 해시 기반의 불안정한 롤백 방식으로 인해 복잡한 이슈가 발생하곤 했습니다. 반면, Docker 환경에서는 각 이미지에 태그 기반의 버전 관리를 적용할 수 있어, 명확한 버전 이력 추적과 빠른 롤백이 가능해졌습니다.

  • 환경 일관성 유지

개발 환경과 운영 환경 간의 설정 차이로 인해 발생하는 빌드 실패 및 서비스 오류 문제를 해소하고자, Docker 이미지 내에 모든 실행 환경을 패키징하였습니다. 이를 통해 어디서든 동일한 환경에서의 실행을 보장하며, 배포 오류를 줄일 수 있었습니다.

2.3 Terraform을 활용한 IaC 적용 이유

Terraform은 인프라스트럭처를 코드로 정의할 수 있는 도구로, 복잡한 클라우드 리소스를 안정적이고 반복적으로 관리할 수 있는 기반을 제공합니다. 본 프로젝트에서는 다음과 같은 이유로 Terraform을 적극 도입하였습니다:

  • 버전 관리 및 변경 추적

모든 인프라 설정을 코드로 관리하여 인프라 구성 상태를 명확하게 파악할 수 있고, Git을 통한 변경 이력 추적이 가능하여 리뷰 및 협업이 용이합니다.

  • 자동화된 배포 파이프라인 통합

GitHub Actions와 CI/CD 파이프라인과 연계하여 terraform plan 및 apply를 자동으로 실행함으로써, 배포 과정을 전자동화하고 운영 효율성을 향상시킬 수 있습니다.

  • 정책 및 의존성 명시적 정의

관리해야 할 리소스의 수가 증가에 따라 수동 관리의 복잡도와 오류 가능성이 커졌습니다. Terraform을 통해 리소스 간의 의존 관계과 보안 정책을 코드로 명확히 정의함으로써, 인프라 구성을 체계적이고 일관되게 관리할 수 있게 되었습니다.

  • Terraform 기반 Blue-Green 배포 전략 구현

GCP는 AWS와 달리 Blue-Green 배포를 기본적으로 지원하는 관리형 서비스가 부족하기 때문에, Terraform을 활용하여 MIG와 Load Balancer 구성, 트래픽 분산 정책 등을 자동화함으로써 자체적인 Blue-Green 배포 구조를 구축할 수 있습니다. 이를 통해 무중단 배포 및 안전한 롤백 전략을 실현할 수 있습니다.

  • 비용 최적화 및 자원 추적

개발 환경에서는 필요 시에만 인프라를 apply하고, 이후 destroy하여 불필요한 자원 사용을 줄이고 비용을 절감할 수 있었습니다.

  • 모듈화 기반의 표준화 및 재사용성

단일 서버와 달리 관리해야하는 인프라가 많아진만큼 변경사항이 생겼을 때 반영해야하는 작업이 많아졌습니다. Terraform을 통해 모듈을 선언하면, 모듈에 수정사항을 반영하여 모듈을 사용하는 모든 환경에 수정사항을 반영할 수 있습니다.

3. 클라우드 인프라 구성

도메인 서비스(AWS Route 53), 시크릿 키 관리(AWS SSM), 컨테이너 이미지 저장소(AWS ECR)를 제외한 모든 리소스는 GCP 기반으로 구축하였습니다.

3.1 환경 분리 전략

서비스 안정성과 개발 효율성을 위해 개발환경(DEV)과 운영환경(PROD)을 명확히 분리했습니다.

<GCP에 맞춰 다시 그린 아키텍처 이미지>

DEV 환경은 기능·배포 테스트 용도로 사용되며, 소규모 리소스 구성으로 비용 최적화에 집중되었습니다.

PROD 환경은 사용자 대상의 실제 서비스 환경으로, 가용성·보안·확장성 기준에 맞춰 구성되었습니다.

3.2 DNS 서비스 (AWS: Route53)

향후 AWS 전환을 고려하여 DNS는 AWS Route 53을 통해 관리합니다.

  • 도메인hertz-tuning.com(2026년 4월 29일)
  • 레코드 유형: A 타입 (IPv4)
  • 호스팅 대상: Production 환경 GCP HTTP(S) Load Balancer
  • 라우팅 정책: 장애 조치(서비스 오리진 페이지와 장애 안내 페이지)

3.3 HTTPS 및 인증서 구성

GCP의 Managed SSL Certificate 기능을 활용하여 인증서를 자동으로 발급 및 갱신하도록 구성하였습니다.

HTTPS 적용 방식

  • SSL Termination: GCP의 HTTPS Load Balancer에서 SSL 종료 처리
  • HTTP to HTTPS 리디렉션: GCP Load Balancer의 URL Map에서 HTTP 요청은 HTTPS로 리디렉션되도록 설정

3.4 VPC 및 Network

Docker - 네트워크

3.4.1 Region

  • 사용 Region: asia-east1 (Taiwan)
  • 선정 사유: asia-northeast3(서울) 리전에 비해 리소스 비용이 상대적으로 낮아, 동일 성능 대비 운영비용 절감 효과를 고려하여 선택하였습니다.

3.4.2 Subnet

공개 범위 CIDR 블록 적용 계층(3-Tier)
Public 10.10.1.0/24
10.10.2.0/24
Presentation
Private 10.10.11.0/24
10.10.12.0/24
Data(DB)
Nat-Private 10.10.21.0/24
10.10.22.0/24
Application
  • Public Subnet: 외부 인터넷 통신이 필요한 프론트엔드 관련 자원에 할당
  • Private Subnet: DB 서버 전용으로 외부 접근 제한
  • Nat-Private Subnet: 인터넷 접근이 필요한 백엔드·Socket.IO 서버 등에 NAT 게이트웨이와 함께 구성

3.4.3 Available Zone

  • asia-east1-a
  • asia-east1-b

두 개의 가용영역에 리소스를 분산 배치하여 고가용성(HA) 및 장애 시 자동 복구 전략을 구현하였습니다.

3.5 방화벽

방화벽 규칙 요약

이름 설명 프로토콜 / 포트 Source Target Tags
internal-all VPC 내부 모든 트래픽 허용 ALL public, private, nat 서브넷 전체 CIDR 없음
ingress-public 외부 SSH, HTTP, HTTPS 허용 TCP: 22, 80, 443 0.0.0.0/0 allow-ssh-http
ingress-openvpn OpenVPN UDP 트래픽 허용 UDP: 1194 0.0.0.0/0 openvpn
openvpn-console OpenVPN 웹 UI 접근 허용 TCP: 943, 443 0.0.0.0/0 openvpn
ssh-from-vpn VPN 사용자 → SSH 허용 TCP: 22 172.27.224.0/20 allow-vpn-ssh
${vpc_name}-fw-lb-to-frontend 외부 HTTPS LB → Frontend 접근 허용 TCP: 80 130.211.0.0/22, 35.191.0.0/16 frontend
${vpc_name}-fw-lb-to-backend 외부 HTTPS LB → Backend 접근 허용 TCP: 8080 130.211.0.0/22, 35.191.0.0/16 backend
${var.env}-fw-frontend-to-backend Frontend → Backend 접근 허용 TCP: 8080 frontend backend
${var.env}-fw-backend-to-mysql Backend → MySQL 접근 허용 TCP: 3306 backend mysql
${var.env}-fw-befe-to-websocket Frontend/Backend → WebSocket 접근 허용 TCP: 9092 backend, frontend websocket
${var.env}-fw-backend-to-redis Backend/WebSocket → Redis 접근 허용 TCP: 6379 backend, websocket redis

3.6 Load Balancer

서비스의 외부 접근 지점은 GCP의 Global HTTP(S) Load Balancer를 통해 구성하였습니다. 프론트엔드, 백엔드, WebSocket 각 계층을 경로 기반(URL path)으로 분기하며, Blue-Green 배포 전략과 연계된 Target Group(weighted backend service) 구조를 통해 무중단 배포 및 트래픽 조절이 가능하도록 설계되었습니다.

3.6.1 Routing Path

요청 도메인: https://hertz-tuning.com/

경로 매칭 프로토콜 목적 어플리케이션
/ HTTPS 프론트엔드 서비스 Next.JS
/api/* HTTPS 백엔드 서비스 SpringBoot
/swagger-ui/* HTTPS 백엔드 서비스 SpringBoot
/v3/* HTTPS 백엔드 서비스 SpringBoot
/ws/* HTTPS 웹소켓 SpringBoot(Netty + netty-socketio)
/ws/* HTTPS 웹소켓 SpringBoot(Netty + netty-socketio)

3.6.2 헬스체크

  • Frontend → /
  • Backend → /api/ping
  • Web socket→ /ws/ping

3.7 Auto Scaling(MIG) & Compute Instance

서비스의 주요 컴포넌트는 GCP의 Compute Engine 기반으로 구성되었으며, 운영 환경(PROD)에서는 Managed Instance Group(MIG)을 기반으로 오토스케일링을 적용하여 고가용성과 확장성을 확보하였습니다. 개발 환경(DEV) 역시 MIG 구조를 유지하되, 인스턴스 수를 고정하여 비용 효율성을 우선하는 전략을 채택하였습니다.

3.7.1 Auto Scaling(MIG) 구성

GCP의 Regional MIG와 Autoscaler를 활용하여, 트래픽 변화에 따라 인스턴스 수를 자동으로 확장 또는 축소할 수 있도록 구성하였습니다. 이를 통해 트래픽 급증 시 자동 확장을, 유휴 시 자원 축소를 통해 비용 최적화를 실현합니다. 또한 Blue-Green 배포 구조와 연동되어 새로운 인스턴스 그룹에도 동일한 Auto Scaling 정책이 적용되며, 점진적 배포와 무중단 운영이 가능합니다.

항목 설정 값 설명
스케일링 기준 CPU 사용률 평균 CPU 사용률 기준으로 동작
목표 사용률 80% 그룹 전체 평균이 80%를 넘으면 확장
최소 인스턴스 수 1 리소스 절약을 위한 하한 설정
최대 인스턴스 수 2 과도한 확장을 방지하기 위한 상한 설정
쿨다운 기간 300초 연속적인 스케일링을 방지하기 위한 지연 시간
축소 제어 정책 최대 1개만 축소 PROD 환경에서 안정성 확보를 위해 급격한 축소 방지
드레이닝 기간 300초 인스턴스 축소 또는 교체 시 기존 연결을 유지한 채 안전하게 종료하기 위한 시간
  • CPU 기반 스케일링 정책: 평균 CPU 사용률이 정의된 임계치를 초과하면 인스턴스를 확장하고, 임계치 미만이 일정 시간 지속되면 축소
  • 최소/최대 인스턴스 수 제한: 서비스 안정성과 비용 제어를 위해 min/max 인스턴스 수 설정
  • 쿨다운 기간 설정: 빈번한 스케일링을 방지 및 초기 세팅 시 올라가는 CPU 점유율을 무시하기 위한 쿨다운 기간 적용
  • 드레이닝 기간 설정: Blue-Green에서 MIG 교체 시, 기존 트래픽을 안정적으로 교체하기 위한 설정

오토스케일링은 Frontend와 Backend에만 적용됩니다.

3.7.2 Instance 구성

컴포넌트 머신 타입 이미지 디스크 타입 디스크 애플리케이션 구성
OpenVPN e2-small ubuntu-os-cloud/ubuntu-2204-lts PERSISTENT
(pd-balanced) 10GB OpenVPN
Frontend e2-small Base 이미지 PERSISTENT
(pd-balanced) 20GB Next.js
Backend e2-medium Base 이미지 PERSISTENT
(pd-balanced) 20GB Spring Boot, WebSocket(Netty + netty-socketio)
DB e2-small ubuntu-os-cloud/ubuntu-2204-lts PERSISTENT
(pd-balanced, pd-ssd) 10GB, 30GB(별도 마운트) MySQL, Redis

DB의 경우, 사용자 데이터가 저장되는 별도의 디스크가 마운트 됩니다. 해당 디스크는 PROD Terraform 환경과 별도로 관리됩니다.

3.7.3 초기화 및 템플릿

모든 인스턴스는 Base 머신이미지를 기반으로 서비스별 초기화 스크립트를 적용하여 초기화됩니다. SSH 키, 서비스 계정 정보 등 민감한 데이터는 변수 기반으로 동적으로 삽입됩니다.

  • Base 머신이미지: ubuntu-2204-lts에 aws-cli 및 docker가 설치된 이미지

  • 서비스 계정 및 SSH키: deploydeploy Public Key

(Frontend) 인스턴스 초기화 스크립트
#!/bin/bash
set -e

# 로그 기록
exec > >(tee -a /var/log/base-init.log) 2>&1

echo "========== 기본 초기화 시작 =========="

# deploy 사용자 생성 및 SSH 키 등록
if id "deploy" &>/dev/null; then
  echo "[INFO] deploy 사용자 이미 존재함"
else
  useradd -m -s /bin/bash deploy
  mkdir -p /home/deploy/.ssh
fi
# deploy 사용자 생성 및 SSH 키 등록
echo "${deploy_ssh_public_key}" > /home/deploy/.ssh/authorized_keys
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
# deploy 사용자에 제한 sudo 권한 부여 (docker / openvpnas 제어용)
if ! grep -q "deploy" /etc/sudoers; then
  echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl * docker, /bin/systemctl * openvpnas, /bin/service openvpnas *" >> /etc/sudoers
fi

# deploy 사용자에 docker 그룹 권한 부여
usermod -aG docker deploy

echo "[INFO] 기본 초기화 완료"

%{ if use_ecr == "true" }
echo "[INFO] ECR 로그인 설정 중..."

# Docker 서비스가 완전히 시작될 때까지 대기
echo "[INFO] Docker 서비스 시작 대기 중..."
sleep 10

# Docker 서비스 상태 확인
systemctl is-active docker || {
	echo "[ERROR] Docker 서비스가 실행되지 않음"
	systemctl status docker
	exit 1
}

# AWS 자격 증명 설정
export AWS_ACCESS_KEY_ID="${aws_access_key_id}"
export AWS_SECRET_ACCESS_KEY="${aws_secret_access_key}"
export AWS_REGION="${aws_region}"

# ECR 레지스트리 URL 추출
ECR_REGISTRY=$(echo "${docker_image}" | cut -d'/' -f1)
echo "[INFO] ECR 레지스트리: $ECR_REGISTRY"

# ECR 로그인
echo "[INFO] ECR에 로그인 중..."
aws ecr get-login-password --region ${aws_region} | docker login --username AWS --password-stdin $ECR_REGISTRY

if [ $? -eq 0 ]; then
	echo "[SUCCESS] ECR 로그인 성공"
else
	echo "[ERROR] ECR 로그인 실패"
	# 디버깅을 위한 추가 정보
	echo "[DEBUG] 사용된 AWS 리전: ${aws_region}"
	echo "[DEBUG] ECR 레지스트리: $ECR_REGISTRY"
	aws sts get-caller-identity || echo "[ERROR] AWS 자격 증명 확인 실패"
	exit 1
fi

ENV_FILE="/home/deploy/app.env"
DIR_PATH=$(dirname "$ENV_FILE")
if [ ! -d "$DIR_PATH" ]; then
  mkdir -p "$DIR_PATH"
  chown $(whoami) "$DIR_PATH"
fi

> "$ENV_FILE"
echo "# nextjs 환경변수" >> "$ENV_FILE"

# SSM 파라미터 prefix
SSM_PATH="${ssm_path}"

PARAM_JSON=$(aws ssm get-parameters-by-path \\
  --path "$SSM_PATH" \\
  --recursive \\
  --with-decryption \\
  --region "$AWS_REGION" \\
  --output json)
echo "$PARAM_JSON" | jq -r '.Parameters[] | "\\(.Name | ltrimstr("'"$SSM_PATH"'"))=\\(.Value)"' >> "$ENV_FILE"

echo "✅ SSM 파라미터를 $ENV_FILE 파일로 저장 완료"

# 이미지 변수 설정 (ECR 이미지)
export IMAGE="${docker_image}"
echo "[INFO] ECR 이미지 사용: $IMAGE"

%{ else }
# 일반 Docker Hub 이미지 사용
export IMAGE="${docker_image}"
echo "[INFO] Docker Hub 이미지 사용: $IMAGE"
%{ endif }

export IMAGE=$(echo "$IMAGE" | tr -d '[:space:]')
echo "[INFO] 최종 Docker 이미지: $IMAGE"

# 기존 컨테이너 정리
echo "[INFO] 기존 '${container_name}' 컨테이너 정리 중..."
docker rm -f ${container_name} 2>/dev/null || true

# Docker 이미지 pull
echo "[INFO] Docker 이미지 pull 시작..."
docker pull "$IMAGE"

if [ $? -eq 0 ]; then
	echo "[SUCCESS] 이미지 pull 성공"
	
	# 새 컨테이너 실행
	echo "[INFO] 새 컨테이너 실행 중..."
	docker run -d \\
		--name ${container_name} \\
		--restart always \\
		--env-file $ENV_FILE \\
		-p ${host_port}:${container_port} \\
		"$IMAGE"
	
	if [ $? -eq 0 ]; then
		echo "[SUCCESS] 컨테이너 실행 성공"
		echo "[INFO] 컨테이너 상태:"
		docker ps | grep ${container_name}
		
		echo "[INFO] 컨테이너 로그 (최근 10줄):"
		docker logs --tail 10 ${container_name}
		
		# deploy 사용자도 Docker 명령을 사용할 수 있도록 권한 설정
		echo "[INFO] deploy 사용자 Docker 권한 설정..."
		usermod -aG docker deploy
		
		# docker.sock 권한 설정
		chmod 666 /var/run/docker.sock
		
	else
		echo "[ERROR] 컨테이너 실행 실패"
		docker logs ${container_name} 2>/dev/null || echo "[ERROR] 컨테이너 로그를 가져올 수 없음"
		exit 1
	fi
else
	echo "[ERROR] 이미지 pull 실패"
	echo "[DEBUG] Docker 데몬 상태:"
	systemctl status docker --no-pager -l
	exit 1
fi

echo "======================================================"
echo "[SUCCESS] 모든 초기화 작업 완료"
(Backend) 인스턴스 초기화 스크립트
#!/bin/bash
set -e

# 로그 기록
exec > >(tee -a /var/log/base-init.log) 2>&1

echo "========== 기본 초기화 시작 =========="

if id "deploy" &>/dev/null; then
  echo "[INFO] deploy 사용자 이미 존재함"
else
  useradd -m -s /bin/bash deploy
  mkdir -p /home/deploy/.ssh
fi
# deploy 사용자 생성 및 SSH 키 등록
echo "${deploy_ssh_public_key}" > /home/deploy/.ssh/authorized_keys
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
# deploy 사용자에 제한 sudo 권한 부여 (docker / openvpnas 제어용)
if ! grep -q "deploy" /etc/sudoers; then
  echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl * docker, /bin/systemctl * openvpnas, /bin/service openvpnas *" >> /etc/sudoers
fi

# deploy 사용자에 docker 그룹 권한 부여
usermod -aG docker deploy

echo "[INFO] 기본 초기화 완료"

%{ if use_ecr == "true" }
echo "[INFO] ECR 로그인 설정 중..."

# Docker 서비스가 완전히 시작될 때까지 대기
echo "[INFO] Docker 서비스 시작 대기 중..."
sleep 10

# Docker 서비스 상태 확인
systemctl is-active docker || {
	echo "[ERROR] Docker 서비스가 실행되지 않음"
	systemctl status docker
	exit 1
}

# AWS 자격 증명 설정
export AWS_ACCESS_KEY_ID="${aws_access_key_id}"
export AWS_SECRET_ACCESS_KEY="${aws_secret_access_key}"
export AWS_REGION="${aws_region}"

# ECR 레지스트리 URL 추출
ECR_REGISTRY=$(echo "${docker_image}" | cut -d'/' -f1)
echo "[INFO] ECR 레지스트리: $ECR_REGISTRY"

# ECR 로그인
echo "[INFO] ECR에 로그인 중..."
aws ecr get-login-password --region ${aws_region} | docker login --username AWS --password-stdin $ECR_REGISTRY

if [ $? -eq 0 ]; then
	echo "[SUCCESS] ECR 로그인 성공"
else
	echo "[ERROR] ECR 로그인 실패"
	# 디버깅을 위한 추가 정보
	echo "[DEBUG] 사용된 AWS 리전: ${aws_region}"
	echo "[DEBUG] ECR 레지스트리: $ECR_REGISTRY"
	aws sts get-caller-identity || echo "[ERROR] AWS 자격 증명 확인 실패"
	exit 1
fi

ENV_FILE="/home/deploy/app.env"
DIR_PATH=$(dirname "$ENV_FILE")
if [ ! -d "$DIR_PATH" ]; then
  mkdir -p "$DIR_PATH"
  chown $(whoami) "$DIR_PATH"
fi

> "$ENV_FILE"
echo "# Spring Boot 환경변수 (SSM→.env, DB_HOST는 로컬 IP로 덮어쓰기)" >> "$ENV_FILE"

# SSM 파라미터 prefix
SSM_PATH="${ssm_path}"

PARAM_JSON=$(aws ssm get-parameters-by-path \\
  --path "$SSM_PATH" \\
  --recursive \\
  --with-decryption \\
  --region "$AWS_REGION" \\
  --output json)
echo "$PARAM_JSON" | jq -r '.Parameters[] | "\\(.Name | ltrimstr("'"$SSM_PATH"'"))=\\(.Value)"' >> "$ENV_FILE"

echo "✅ SSM 파라미터를 $ENV_FILE 파일로 저장 완료"
LOCAL_DB_HOST="${db_host}"
echo "DB_HOST=$LOCAL_DB_HOST" >> "$ENV_FILE"

# 이미지 변수 설정 (ECR 이미지)
export IMAGE="${docker_image}"
echo "[INFO] ECR 이미지 사용: $IMAGE"

%{ else }
# 일반 Docker Hub 이미지 사용
export IMAGE="${docker_image}"
echo "[INFO] Docker Hub 이미지 사용: $IMAGE"
%{ endif }

# 기존 컨테이너 정리
echo "[INFO] 기존 '${container_name}' 컨테이너 정리 중..."
docker rm -f ${container_name} 2>/dev/null || true

# Docker 이미지 pull
echo "[INFO] Docker 이미지 pull 시작..."
docker pull "$IMAGE"

if [ $? -eq 0 ]; then
	echo "[SUCCESS] 이미지 pull 성공"
	
	# 새 컨테이너 실행
	echo "[INFO] 새 컨테이너 실행 중..."
	 docker run -d \\
		--name ${container_name} \\
		--restart always \\
		--env-file $ENV_FILE \\
		-p ${host_port}:${container_port} \\
		-p 9092:9092 \\
		"$IMAGE"
	
	if [ $? -eq 0 ]; then
		echo "[SUCCESS] 컨테이너 실행 성공"
		echo "[INFO] 컨테이너 상태:"
		docker ps | grep ${container_name}
		
		echo "[INFO] 컨테이너 로그 (최근 10줄):"
		docker logs --tail 10 ${container_name}
		
		# deploy 사용자도 Docker 명령을 사용할 수 있도록 권한 설정
		echo "[INFO] deploy 사용자 Docker 권한 설정..."
		usermod -aG docker deploy
		
		# docker.sock 권한 설정
		chmod 666 /var/run/docker.sock
		
	else
		echo "[ERROR] 컨테이너 실행 실패"
		docker logs ${container_name} 2>/dev/null || echo "[ERROR] 컨테이너 로그를 가져올 수 없음"
		exit 1
	fi
else
	echo "[ERROR] 이미지 pull 실패"
	echo "[DEBUG] Docker 데몬 상태:"
	systemctl status docker --no-pager -l
	exit 1
fi

echo "======================================================"
echo "[SUCCESS] 모든 초기화 작업 완료"
(DB) 인스턴스 초기화 스크립트
#!/bin/bash
set -e

# 로그 기록
exec > >(tee -a /var/log/base-init.log) 2>&1

echo "========== 기본 초기화 시작 =========="

# deploy 사용자 생성 및 SSH 키 등록
if id "deploy" &>/dev/null; then
  echo "[INFO] deploy 사용자 이미 존재함"
else
  echo "[INFO] deploy 사용자 생성 및 SSH 키 등록"
  useradd -m -s /bin/bash deploy
  mkdir -p /home/deploy/.ssh
  echo "${deploy_ssh_public_key}" > /home/deploy/.ssh/authorized_keys
  chmod 700 /home/deploy/.ssh
  chmod 600 /home/deploy/.ssh/authorized_keys
  chown -R deploy:deploy /home/deploy/.ssh
fi

# deploy 사용자에 제한 sudo 권한 부여 (docker / openvpnas 제어용)
if ! grep -q "deploy" /etc/sudoers; then
  echo "deploy ALL=(ALL) NOPASSWD: /bin/systemctl * docker, /bin/systemctl * openvpnas, /bin/service openvpnas *" >> /etc/sudoers
fi

echo "[INFO] 기본 초기화 완료"
DEVICE="/dev/disk/by-id/google-mysql-data"
MOUNT_POINT="/mnt/tmp"

# 1. 마운트 디렉토리 생성
sudo mkdir -p $MOUNT_POINT

# 2. 디스크가 ext4로 포맷되어 있지 않다면, 포맷 (데이터 모두 삭제됨, 최초 1회만!)
if ! sudo blkid $DEVICE | grep -q 'ext4'; then
  sudo mkfs.ext4 -F $DEVICE
fi

# 3. /etc/fstab에 등록 (중복 방지)
if ! grep -q "$DEVICE" /etc/fstab; then
  echo "$DEVICE $MOUNT_POINT ext4 defaults 0 2" | sudo tee -a /etc/fstab
fi

# 4. 즉시 마운트 (fstab에 등록된 대로)
sudo mount $MOUNT_POINT

# 5. mysql-data 서브디렉토리 생성
sudo mkdir -p $MOUNT_POINT/mysql-data

# 로그 설정
exec > >(tee /var/log/user-data.log) 2>&1
echo "======================================================"
# Docker 설치 (공식 스크립트 사용)
dpkg -i $${MOUNT_POINT}/containerd.io_*.deb $${MOUNT_POINT}/docker-ce-cli_*.deb $${MOUNT_POINT}/docker-ce_*.deb || apt-get install -f -y

# deploy 사용자에 docker 그룹 권한 부여
usermod -aG docker deploy

docker load -i $${MOUNT_POINT}/mysql-8.0.tar

docker run -d \\
  --name mysql \\
  --restart always \\
  -e MYSQL_ROOT_PASSWORD="${rootpasswd}" \\
  -e MYSQL_DATABASE="${db_name}" \\
  -e MYSQL_USER="${user_name}" \\
  -e MYSQL_PASSWORD="${rootpasswd}" \\
  -v $${MOUNT_POINT}/mysql-data:/var/lib/mysql \\
  -p 3306:3306 \\
  mysql:8.0

echo "[startup] MySQL container launched with data on $${MOUNT_POINT}"

# Redis 이미지 로드 및 실행
echo "[INFO] Redis 컨테이너 시작"
docker load -i $MOUNT_POINT/redis-7.2.4.tar

docker run -d \\
  --name redis \\
  --restart unless-stopped \\
  -e REDIS_PASSWORD="${redis_password}" \\
  -v $${MOUNT_POINT}/redis_data:/data \\
  -p 6379:6379 \\
  "redis:7.2.4" \\
  redis-server --requirepass "${redis_password}"

3.7.4 Public/Private IP 구성

서비스의 네트워크 보안을 강화하고 역할별 네트워크 분리를 명확히 하기 위해, 퍼블릭 및 프라이빗 IP 할당을 계층에 따라 구분하여 구성하였습니다.

  • Public IP: VPN 접속을 위한 OpenVPN 서버에만 할당됩니다.

    일반 서비스 인스턴스(Frontend, Backend, DB 등)는 퍼블릭 IP를 사용하지 않으며 외부 접근은 Load Balancer를 통해서만 가능하도록 제한됩니다.

  • Private IP: 모든 컴퓨팅 리소스는 소속된 서브넷의 CIDR 대역에 따라 프라이빗 IP를 자동 할당받습니다.

    각 서브넷은 다음과 같은 역할로 분리되어 있습니다:

    서브넷 타입 용도
    Public Subnet 외부 노출 자원(Next.JS)
    NAT-Private NAT 필요 자원(SpringBoot, WebSocket)
    Private Subnet 외부 비노출 자원(MySQL, Redis)

4. Docker 기반 어플리케이션 구조

4.1 어플리케이션 구성 방식

모든 주요 컴포넌트는 Docker 컨테이너로 분리되어 운영됩니다. 각 서비스는 별도의 VM 인스턴스에서 실행되며, 다음과 같은 방식으로 구성됩니다:

컴포넌트 Docker화 여부 설명
Frontend (Next.js) pnpm을 통한 Next.JS 앱 처리
Backend (Spring Boot) REST API 및 WebSocket(Netty + Socket.IO) 처리
AI 서버 (FastAPI) 사용자 추천 및 임베딩 FastAPI
AI GPU 서버 (FastAPI) PM2를 이용한 FastAPI 및 VLLM 실행
DB (MySQL, Redis) MySQL, Redis 컨테이너 실행

모든 애플리케이션 컨테이너는 독립적으로 배포되며, 서로 간 통신은 내부 VPC IP 및 로드밸런서를 통해 이루어집니다.

4.2 Dockerfile 및 docker-compose.yml 구조

  • DB를 제외한 각 서비스는 개별 Dockerfile을 보유하며, Github Actions 기반 CI 과정에서 이미지를 빌드합니다.

Dockerfile

Frontend(Next.JS)

# 1단계: Build 단계
FROM node:20-alpine AS builder
RUN npm install -g pnpm
WORKDIR /app

# 종속성 설치만을 위한 레이어 캐시
COPY pnpm-lock.yaml package.json ./
RUN pnpm install

# 소스 복사 (이때까지 캐시 최대한 활용)
COPY . .

# 🔥 환경변수는 여기서 주입 (캐시가 깨져도 최소 영향)
ARG NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}

RUN pnpm build

# 2단계: 실행용 이미지
FROM node:20-alpine
RUN npm install -g pnpm
WORKDIR /app

# 빌드된 결과 복사
COPY --from=builder /app ./

EXPOSE 3000
CMD ["pnpm", "start"]

Backend(SpringBoot)

# 1단계: 빌드 이미지
FROM gradle:8.5-jdk21 AS builder

WORKDIR /app
COPY . .
RUN ./gradlew clean build -x test

# 2단계: 실행 이미지
FROM eclipse-temurin:21-jdk

WORKDIR /app

# 빌드된 JAR 복사 (단일 JAR 가정)
COPY --from=builder /app/build/libs/*.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

AI 매칭 API(FastAPI)

FROM python:3.10-slim

# 작업 디렉토리 지정
WORKDIR /app

# 필수 시스템 패키지 설치 및 캐시 제거
RUN apt-get update \\
	&& apt-get install -y --no-install-recommends libsqlite3-dev libjemalloc2 libjemalloc-dev \\
	&& rm -rf /var/lib/apt/lists/*

# pip 최신화 및 빌드 도구 설치 (실패율↓, 설치속도↑)
RUN pip install --upgrade pip setuptools wheel

# requirements-base.txt만 복사 (캐시 최대 활용)
COPY ./app-tuning/requirements-base.txt /app/requirements-base.txt

# 의존성 설치 (최소 requirements만)
RUN pip install --no-cache-dir -r /app/requirements-base.txt

# 앱 소스 복사 (최대한 마지막에! 코드 변경 시 캐시 최대화)
COPY ./app-tuning /app

# 모델 캐시 폴더, 기타 필요시만 생성
RUN mkdir -p /app/model-cache

# 환경변수 설정 (한 번에, 중복 없이!)
ENV PYTHONUNBUFFERED=1 \\
	PYTHONDONTWRITEBYTECODE=1 \\
	PYTHONPATH=/app:/app/extlibs \\
	ENVIRONMENT=prod \\
	CHROMA_MODE=server \\
	CHROMA_HOST=host.docker.internal \\
	CHROMA_PORT=8001 \\
	ALLOW_RESET=false \\
	PORT=8000 \\
	OMP_NUM_THREADS=1 \\
	MKL_NUM_THREADS=1 \\
	NUMEXPR_NUM_THREADS=1 \\
	OPENBLAS_NUM_THREADS=1 \\
	LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

4.3 이미지 빌드 및 레지스트리 관리

  • 각 서비스의 Docker 이미지는 GitHub Actions의 CI 과정에서 자동으로 빌드됩니다.
  • 빌드된 이미지는 AWS Elastic Container Registry(ECR)에 저장되며, 서비스별로 다음과 같은 저장소를 사용합니다:

4.3.1 ECR 저장소 명

서비스 ECR 저장소 이름
Next.js (Frontend) tuning-nextjs
Spring Boot (Backend) tuning-springboot
FastAPI (AI Server) tuning-api
ChromaDB tuning-chromadb

4.3.2 태그 규칙

이미지 태그는 Branch 및 Commit 정보를 기준으로 다음과 같이 구성됩니다:

  • ${REPOSITORY}:${BRANCH}-${SHORT_SHA} 예: tuning-backend:develop-a1b2c3d
  • ${REPOSITORY}:main-latest: main 브랜치의 최신 이미지에 부여
  • ${REPOSITORY}:develop-latest : develop 브랜치의 최신 이미지에 부여

4.4 환경변수 및 Secret 구성 방식

  • 모든 민감 정보와 환경 변수는 AWS SSM Parameter StoreGitHub Secrets에 저장되며, 배포 시 GitHub Actionsaws-cli를 통해 참조됩니다.
  • 각 서비스는 실행 환경에 따라 필요한 값을 SSM에서 동적으로 로드합니다.

4.4.1 SSM Parameter Store 구성

환경 서비스 파라미터 경로 형식
PROD Spring Boot /global/springboot/prod/*
DEV Spring Boot /global/springboot/dev/*
PROD Next.js /global/nextjs/prod/*
DEV Next.js /global/nextjs/dev/*
PROD/DEV GCP 관련 설정 /global/gcp/*
그 외 GitHub, Terraform Cloud API 키 /global/GITHUB_TOKEN /global/TFC_TOKEN

SSM 파라미터는 서비스 및 환경별로 구분되어 있으며, 텍스트 형식으로 저장됩니다. 민감한 데이터의 경우 SecureString으로 등록되어 있습니다.

4.4.2 GitHub Secrets 구성

  • AWS 접근을 위한 Access Key 및 Secret Key
  • Discord Webhook URL (배포 성공/실패 알림)

Secret 키는 모두 암호화되어 저장되며, GitHub Actions 로그에는 출력되지 않도록 설정되어 있습니다.

5. IaC(Terraform)

본 프로젝트의 모든 인프라는 코드 기반으로 선언되어 있으며, Terraform을 이용하여 모듈화 및 환경 분리를 통해 재사용성과 유지보수성을 높였습니다.

5.1 모듈 구성

Terraform은 기능 단위로 모듈화되어 있으며, 각 모듈은 다음과 같은 컴포넌트를 정의합니다:

모듈 이름 주요 리소스
compute VM 인스턴스
external-https-lb HTTPS Load Balancer 구성 (URL Map, SSL 인증서 포함)
firewall GCP Firewall Rule
health-check GCP Health Check
mig-asg 인스턴스 템플릿, Managed Instance Group, Autoscaler
network VPC 및 Subnet
target-group Backend 서비스 연결 설정

각 모듈은 environments/{env} 디렉토리에서 호출되며, 환경(dev, prod 등)에 따라 필요한 파라미터를 주입하여 독립적으로 동작합니다.

NAT Gateway, Database, OpenVPN 등 일부 리소스는 환경별 요구사항이 달라, main.tf 내에서 직접 정의하여 개별적으로 관리합니다.

5.2 환경별 변수 및 상태관리

Terraform 변수는 환경별로 variable.tf에 명시되어 있으며, 실제 변수 값은 Terraform Cloud의 Variable에서 관리합니다.

5.2.1 환경별 디렉터리

  • environments/dev/ : 개발 환경 배포 구성
  • environments/prod/ : 운영 환경 배포 구성
  • environments/shared-dev/ : 개발 환경 VPC 및 고정 리소스 구성
  • environments/shared/ : 운영 환경 VPC 및 고정 리소스 구성

5.2.2 상태관리(State)

상태 관리는 Terraform Cloud의 Workspace를 통해 환경마다 분리되며, 다음과 같이 구성됩니다:

환경 코드 디렉터리 Workspace 이름
개발 environments/dev/ gcp-dev
운영 environments/prod/ gcp-prod
개발 환경 VPC 및 고정 리소스 구성 environments/shared-dev/ shared-dev
운영 환경 VPC 및 고정 리소스 구성 environments/shared/ gcp-shard

terraform_remote_state를 통해 공유된 상태를 GitHub Actions 환경에서 참조합니다.

5.3 Terraform Cloud 연동 및 Remote Backend 관리

Terraform Cloud를 사용하여 다음과 같은 이점을 확보하였습니다:

  • 협업 효율성 향상: 모든 환경의 Terraform 실행 이력이 웹 대시보드에서 공유되며, 동시에 여러 사용자가 작업하더라도 상태 잠금(Locking) 기능을 통해 충돌 없이 안전하게 관리할 수 있습니다.
  • Remote Backend: 각 환경의 상태파일은 Terraform Cloud에서 안전하게 관리됩니다.
  • Workspace 별 분리: dev, prod, shared 등의 환경을 워크스페이스 단위로 분리하여 병렬 적용 방지 및 충돌 최소화
  • 자동화 통합: GitHub Actions에서 Terraform 명령어(init, plan, apply)를 자동으로 실행하며, 필요 시 수동 승인도 지원합니다.
  • 보안 강화: 환경변수와 시크릿은 Terraform Cloud의 환경 설정 또는 AWS SSM을 통해 안전하게 주입됩니다.

모든 배포는 GitHub Actions를 통해 terraform apply가 자동으로 수행되며, 버전 및 환경의 일관성을 유지하기 위해 GitHub Actions 외의 환경에서 Terraform을 직접 실행하지 않습니다.

6. 배포 및 운영전략

6.1 Frontend/Backend 배포 전략 및 CI/CD

Frontend 및 Backend 서비스는 GitHub와 GitHub Actions 기반의 파이프라인으로 배포되며, 브랜치 전략에 따라 환경(dev, prod)을 분리하여 CI/CD가 실행됩니다. CI 단계에서는 코드 검증 및 Docker 이미지 빌드가 수행되며, CD 단계에서는 Terraform을 통해 Blue-Green 방식으로 안전하게 배포가 이뤄집니다.

6.1.1 CI (Continuous Integration) 파이프라인

GitHub Actions를 사용하여 main/develop 브랜치의 PR이 Merge 될 때 자동으로 CI 파이프라인이 실행됩니다. 또한 workflow_dispatch를 통해 수동/자동으로 트리거됩니다.

  1. Lint 및 코드 검사: 코딩 컨벤션 및 문법 오류를 자동으로 감지합니다.
  2. 단위 테스트: 서비스별로 설정된 테스트 코드가 실행되어 코드 변경 사항의 안정성을 검증합니다.
  3. Docker 이미지 빌드: 서비스별로 Dockerfile을 기반으로 이미지를 빌드하며, 성공 시 ECR에 푸시됩니다.
  4. 아티팩트 태깅 및 저장: Git SHA를 기준으로 이미지에 태그를 부여하고, 환경별로 latest, main-latest 등으로 관리됩니다.

프론트엔드(Next.js), 백엔드(Spring Boot)는 각각 독립적인 CI 파이프라인을 갖고 있으며, 모든 CI 결과는 Discord Webhook을 통해 알림이 보내지게 됩니다.

6.1.2 CD (Continuous Deployment) 파이프라인

CD 파이프라인은 CI가 정상적으로 완료 됐을 시에 동작합니다.

  • ECR에 저장된 이미지를 기반으로 Terraform을 활용하여 GCP 인프라에 배포가 이루어집니다.
  • 배포 대상은 MIG(Managed Instance Group)이며, Blue-Green 방식의 배포 전략을 통해 무중단 배포가 가능하게 설계되어 있습니다.
  • 주요 과정(*아래 6.3* Blue-Green 배포 및 롤백 전략에서 자세히 설명 ):
    1. Terraform 변수 파일(.tfvars.json) 동적 생성
    2. 현재 활성화된 색상(blue/green)을 확인
    3. 비활성 그룹에 새 이미지 및 MIG의 min/max 개수를 확장하여 Terraform apply를 수행
    4. Apply 후 헬스체크 수행
      • 성공 시 트래픽 전환 → 활성 그룹 변경 및 기존의 사용하던 그룹 비활성화
      • 실패 시 자동 롤백 트리거

배포 성공 여부는 GCP 헬스체크 상태 및 Discord 알림을 통해 실시간으로 모니터링됩니다.

6.2 Blue-Green 배포 및 롤백 전략

Blue-Green 배포는 PROD 환경에서만 이루어지며, DEV 환경에서는 무중단 배포가 필요없기 때문에 아래 Terraform apply를 통해 변경된 이미지만 적용하여 단일 환경에 배포합니다.

6.2.1 Blue-Green 배포 전략

  1. Terraform 리소스가 준비되어 있는지 확인

Terraform State를 통하여 특정 모듈(module.backend_tg )이 준비되어 있는지 확인합니다.

        run: |
          terraform init -reconfigure

          echo "🔍 Terraform 리소스 존재 여부 확인 중..."

          if terraform state list | grep -q "module.backend_tg"; then
            echo "✅ Terraform 리소스가 존재합니다. 바로 배포를 시작합니다."
          else
            echo "⚠️ Terraform 리소스가 없습니다. 전체 apply를 수행합니다."
            terraform apply -auto-approve
          fi
  1. Blue/Green 각 MIG의 인스턴스 최대 개수(max)를 확인하여, 0개가 아닌 MIG를 활성화 MIG(ACTIVE)로 판단하고, 0개인 항목은 비활성화 MIG(TARGET)로 판단합니다.
  2. 비활성화 MIG(TARGET)의 도커 이미지와 인스턴수 개수를 min=1/max=2로 조정하여, 새 이미지를 배포합니다. (apply 대상은 MIG와 오토스케일러로 제한합니다.) 또한 target 지정 시 output이 반영되지 않기 때문에 -refresh-only 옵션을 통해 output만 재반영합니다.
        run: |
          ACTIVE=${{ steps.active.outputs.active }}
          TARGET=${{ steps.color.outputs.target }}

          echo "{" > 01-deployment.auto.tfvars.json
          echo "  \\"docker_image_back_${TARGET}\\": \\"${{ env.IMAGE }}\\"," >> 01-deployment.auto.tfvars.json
          echo "  \\"traffic_weight_${TARGET}_backend\\": 0," >> 01-deployment.auto.tfvars.json
          echo "  \\"traffic_weight_${ACTIVE}_backend\\": 100," >> 01-deployment.auto.tfvars.json
          echo "  \\"${TARGET}_instance_count_backend\\": {\\"min\\": 1, \\"max\\": 2}," >> 01-deployment.auto.tfvars.json
          echo "  \\"${ACTIVE}_instance_count_backend\\": {\\"min\\": 1, \\"max\\": 2}" >> 01-deployment.auto.tfvars.json
          echo "}" >> 01-deployment.auto.tfvars.json

          cat 01-deployment.auto.tfvars.json
          terraform apply -auto-approve \\
            -target=module.backend_tg \\
            -target=module.backend_internal_asg_${TARGET}

          echo "💾 Output 적용 중 ..."
          terraform apply -refresh-only -auto-approve
          terraform output -json

활성화 MIG(ACTIVE)의 변수 값이 변하지 않게, 현재 값(traffic → 100, instance_count → min=1/max=2)을 한 번 더 명시해줍니다.

  1. TARGET MIG 인스턴스의 헬스체크 값을 확인하여, 모든 인스턴스가 헬스체크가 된다면 다음으로 넘어갑니다.
  2. 헬스체크가 정상적으로 되었으므로 로드밸런서의 트래픽을 조정하여, ACTIVE MIG와 TARGET MIG의 트래픽 할당량을 변경해줍니다.
        run: |
          terraform init -reconfigure

          BLUE_MAX=$(terraform output -json | jq -r '.blue_instance_count_backend.value.max')
          GREEN_MAX=$(terraform output -json | jq -r '.green_instance_count_backend.value.max')
          echo "BLUE_MAX-before-half: $BLUE_MAX"
          echo "GREEN_MAX-before-half: $GREEN_MAX"

          ACTIVE=${{ steps.active.outputs.active }}
          TARGET=${{ steps.color.outputs.target }}
          echo "{" > 02-shift-half.auto.tfvars.json
          echo "  \\"traffic_weight_${ACTIVE}_backend\\": 50," >> 02-shift-half.auto.tfvars.json
          echo "  \\"traffic_weight_${TARGET}_backend\\": 50" >> 02-shift-half.auto.tfvars.json
          echo "}" >> 02-shift-half.auto.tfvars.json
          terraform apply -auto-approve \\
            -target=module.backend_tg \\

          echo "💾 Output 적용 중 ..."
          terraform apply -refresh-only -auto-approve
          terraform output -json
  1. 5번과 같은 방법으로 TARGET MIG에만 트래픽이 할당되도록 변경합니다. (ACTIVE:TARGET=0:100 비율)
  2. ACTIVE MIG의 인스턴수 수를 0으로 줄여 비활성화 합니다.
  3. Discord WebHook을 이용하여 배포 여부에 대한 알림을 보냅니다.

6.2.2 롤백 전략

롤백은 CI 또는 CD 과정에서 문제가 생겨 오류가 난다면 실행되도록 하였습니다.

  1. ACTIVE MIG의 트래픽 할당량과 인스턴스 수를 원래대로 되돌립니다.(트래픽:100, 인스턴스 수 min=1/max=2)
  2. Discord WebHook을 이용하여 롤백 여부에 대한 알림을 보냅니다.

6.3 백업 전략

  • 대상: DB VM에 연결된 디스크에 대한 스냅샷
  • 백업 주기: 매일 04시, 하루 3회, 8시간 간격 자동 실행
  • 백업 방식: GCP의 증분 스냅샷 기능 활용
  • 보존 기간: 30일 유지 및 자동 삭제

7. OpenVPN

7.1 구성 목적

  • 외부 개발자가 GCP 내부 프라이빗 IP 대역에 접근할 수 있도록 VPN 기반의 보안 터널링을 제공합니다.
  • 운영 환경 접근은 SSH Key 기반 접근 통제와 병행되어 보안성이 강화됩니다.

7.2 구성 방식

  • OpenVPN Access Server를 별도의 VM 인스턴스에 구성하였으며, 퍼블릭 IP가 부여된 유일한 인스턴스로 설정되어 있습니다.
  • VPN을 통해 접속한 사용자는 GCP 내 Private Subnet 자원(예: DB, 내부 API 등)에 안전하게 접근할 수 있습니다.
  • 해당 인스턴스는 pd-balanced 디스크를 사용하며, e2-small 머신 타입으로 구성되어 기본적인 트래픽을 처리할 수 있도록 설정되어 있습니다.

OpenVPN 인프라는 Terraform에서 직접 구성되며, 환경별 인프라 코드에서 별도로 정의되어 관리됩니다.

7.3 보안 정책

  • GCP Firewall을 통해 OpenVPN 인스턴스의 443 포트(TCP)만 외부에 오픈되어 있으며, CIDR 제한을 통해 접근 제어가 이루어집니다.
  • 관리용 Web UI는 관리자 IP에서만 접근 가능하도록 추가 제어 정책이 적용됩니다.

8. 모니터링 체계

  • 9.1 지표 수집 (CPU, Memory, Network 등)
  • 9.2 도입 도구 (Netdata, SigNoz 등)
  • 9.3 알림 시스템 (Discord Webhook, Slack 연동 등)
  • 9.4 로그 집계 및 분석 (Fluent Bit, Cloud Logging 등)

9. 초기설계 대비 실제적용 차이

⚠️ **GitHub.com Fallback** ⚠️