2025.11.30 ~ 2025.12.8
구성한 아키택처

받은 피드백(루카)
1. nat와 로드밸런서가 같은 서브넷에 주입되어 있는 구조가 맞는가?
2. External service가 vpc 외부에 있는건 맞지만 dns 하위에서 묶여야하는것은 아닌가? ig에 의해서 라우팅이 묶이기 때문
알아두면 좋을 알고리즘
1. load-balancer에서 헬스 테크가 잘 되었다는 의미가 api 1회 성공이 아니라 몇초 단위로 api를 호출하는데 그중 몇회가 성공하면 서버가 살아있는 것이다.
-> 네트워크가 불안정해서 성공과 실패를 반복하거나 하나의 인스턴스에 두개의 vm을 띄우고 헬스체크를 여러개 호출하는데 vm 둘중 하나는 성공, 하는는 실패 일때 위 규칙성을 통해 인스턴스가 죽었다고 판단할 수도 있음
private vm이 인터넷을 이용하기 위한 기술
OutBound
- 패킷 생성
private vm이 외부로 요청을 보냄- 라우팅
subnet의 route table 규칙에 따라 패킷이 NAT Gateway로 전달됨- SNAT 수행
NAT Gateway는 패킷의 source IP를 자신의 public IP로 덮어쓰고 이 매핑 정보를 기록해둠
InBound
고가용성을 보장하기 위한 리버스 프록시
리버스 프록시
클라이언트가 서버의 실제 IP를 모르게 하고, 로드 밸런서가 대신 요청을 받아 서버들에게 전달하는 구조.SPOF(single poinr of failure) 제거
서버가 한대만 있다면 서버가 죽는순간 서비스가 중단되지만, 로드밸런서는 여러대의 서버를 통해 죽은 서버는 제외하고 산 서버로만 트래픽을 보내 서비스가 절대 멈추지 않게함Health Check
서버가 살아있는지 확인하는 작업. 일정 주기로 트래픽을 보내 확인함. 만약 응답이 없거나 에러가 뜨면 해당 서버를 unhealthy로 마킹하고 트래픽 전송을 중단
Balancing Algorithm
트래픽을 나누는 규칙(round robin, least connection, ip hash)
- 클라이언트가 도메인(https://nexus-gg.kro.kr)을 입력
- 도메인이 구글 로드밸런서의 Anycast IP로 변환
- 가장 가까운 구글 엣지 서버로 접속
- 암호화된 https 패킷을 복호화
- 구글의 내부 광케이틀을 타고 서울 리전으로 이동
- 로드 밸런서 설정을 보고 그룹을 구분하여 요청을 전달
- 헬스체크 확인 후 balancing
- 서버로부터 받은 response를 다시 https 암호화후 사용자에게 전달
Load Balancer는 VPC 외부에서 진입점으로, NAT는 VPC 경계에서 출구점으로 기능하며 Public Subnet에 포함 x
서비스의 논리적 구성요소는 맞지만, 인프라/네트워크 관점에서 봤을 때 GCS는 googleapis.com 도메인을 사용하는 외부 관리형 서비스임. 도메인(nexus-gg.kro.kr)의 DNS 레코드가 직접 관여하지 않으므로, 네트워크 계층 구조상 분리하여 표현하는 것이 정확하다고 생각함
피드백을 적용한 최종 도메인
[]
- 이름 : nexus-allow-ssh-iap
목적 : GCP 콘솔에서 SSH를 통해 VM에 접속- 이름 : nexus-gg-vpc-allow-custom
목적 : 내 VPC 내부의 subnet에서 출발한 신호만 허용- 이름 : nexus-gg-vpc-allow-http/https
목적 : 기본적인 웹 요청 수신
네트워크 인터페이스 설정 :

GCP 콘솔에서 ssh를 통해 vm 접속 후
# 시스템 패키지 목록 업데이트
sudo apt-get update
# Docker 설치에 필요한 기본 패키지들 설치
sudo apt-get install ca-certificates curl gnupg lsb-release -y
# Docker GPG 키를 저장할 디렉토리 생성 (권한 755)
sudo mkdir -m 0755 -p /etc/apt/keyrings
# Docker 공식 GPG 키 다운로드 및 저장
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg
--dearmor -o /etc/apt/keyrings/docker.gpg
# GPG 키 파일에 읽기 권한 부여
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Docker 저장소를 apt 소스 목록에 추가
echo "deb [arch=$(dpkg --print-architecture)
signed-by=/etc/apt/keyrings/docker.gpg]
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee
/etc/apt/sources.list.d/docker.list > /dev/null
# 패키지 목록 업데이트
sudo apt-get update
# Docker Engine, CLI, containerd 및 플러그인들 설치
sudo apt-get install docker-ce docker-ce-cli containerd.io
docker-buildx-plugin docker-compose-plugin -y
# ============================================
# Google Cloud SDK 설치
# ============================================
# Google Cloud 저장소 관련 패키지 설치
sudo apt-get install apt-transport-https ca-certificates gnupg -y
# Google Cloud GPG 키 다운로드 및 저장
curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg
--dearmor -o /usr/share/keyrings/cloud.google.gpg
# Google Cloud SDK 저장소 추가 (main 브랜치)
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg]
https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee
/etc/apt/sources.list.d/google-cloud-sdk.list
# 패키지 목록 업데이트
sudo apt-get update
# Google Cloud SDK 설치
sudo apt-get install google-cloud-sdk -y
# ============================================
# Docker 및 GCP Artifact Registry 인증 설정
# ============================================
# Docker를 GCP Artifact Registry(서울 리전)에 인증 설정
gcloud auth configure-docker asia-northeast3-docker.pkg.dev
# ============================================
# 사용자 권한 설정
# ============================================
# 현재 사용자를 docker 그룹에 추가 (sudo 없이 docker 명령 사용 가능)
sudo usermod -aG docker $USER
# setiguy1 사용자를 docker 그룹에 추가
sudo usermod -aG docker setiguy1
# runner 사용자를 docker 그룹에 추가
sudo usermod -aG docker runner
# ============================================
# 설치 확인
# ============================================
# Docker 버전 확인
docker --version
# Google Cloud SDK 버전 확인
gcloud --version
# home 디렉토리가 있는 사용자 목록 확인
cat /etc/passwd | grep home
# 실행 중인 Docker 컨테이너 목록 확인
docker ps
# nexus-spring-container 컨테이너의 로그 확인 (컨테이너가 실행 중인 경우)
docker logs nexus-spring-container
주의! Google Cloud 서비스 계정 사용 시:
프론트엔드 설정
프로토콜 : Https
IP : 고정 IP 예약후 사용
포트 : 443
백엔드 설정
인스턴스 그룹 생성 및 설정: 사용중인 네트워크와 서브넷으로 설정
프로토콜 : Http -> 로드밸런서가 암호화를 해제했으므로 백엔드 서버에는Http로 전달
포트번호 8080
경로 규칙 : 단순 호스트 및 경로 규칙
프록시 전용 서브넷 생성
방화벽 설정
- 이름 : allow-health-check
목적 : Health 체크를 위함
경로 : 130.211.0.0/22, 35.191.0.0/16, 프록시 전용 서브넷 IP
github runner에서 사용할 IAM 설정
다음 4개 권한 부여
Artifact Registry 작성자
목적: docker push
Compute 인스턴스 관리자(v1)
목적: gcloud compute ssh
Compute OS 관리자 로그인
목적: VM 내부에서 sudo 사용
IAP 보안 터널 사용자
목적: IAP 중계 서버를 통한 VM 접속
CI
name: CI - Build and Test
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
# 동일 브랜치의 이전 워크플로우 취소
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle with caching
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run tests with parallel execution
run: |
./gradlew test \
--no-daemon \
--parallel \
--max-workers=4 \
--build-cache
- name: Upload test results (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ github.sha }}
path: build/test-results/
retention-days: 7
- name: Upload coverage reports
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v4
with:
name: coverage-reports-${{ github.sha }}
path: build/reports/
retention-days: 30
build:
name: Build Docker Image
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Load environment variables safely
id: env
run: |
echo "${{ secrets.PROD_ENV_FILE }}" > .env.tmp
while IFS='=' read -r key value || [ -n "$key" ]; do
[[ "$key" =~ ^#.*$ ]] && continue
[[ -z "$key" ]] && continue
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
value="${value%\"}"
value="${value#\"}"
echo "${key}=${value}" >> $GITHUB_OUTPUT
done < .env.tmp
rm -f .env.tmp
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker for Artifact Registry
run: |
gcloud auth configure-docker ${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev/${{ steps.env.outputs.GCP_PROJECT_ID }}/${{ steps.env.outputs.ARTIFACT_REGISTRY_REPO }}/lol-highlight-backend
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable=true
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64
- name: Output image tags
run: |
echo "Built and pushed images:"
echo "${{ steps.meta.outputs.tags }}"
name: CD - Deploy to Production
on:
workflow_run:
workflows: ["CI - Build and Test"]
types:
- completed
branches:
- main
jobs:
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
environment: production
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Load environment variables safely
id: env
run: |
echo "${{ secrets.PROD_ENV_FILE }}" > .env.tmp
while IFS='=' read -r key value || [ -n "$key" ]; do
[[ "$key" =~ ^#.*$ ]] && continue
[[ -z "$key" ]] && continue
key=$(echo "$key" | xargs)
value=$(echo "$value" | xargs)
value="${value%\"}"
value="${value#\"}"
echo "${key}=${value}" >> $GITHUB_OUTPUT
done < .env.tmp
rm -f .env.tmp
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Get latest image tag
id: image
run: |
IMAGE_TAG="${{ steps.env.outputs.GCP_REGION }}-docker.pkg.dev/${{ steps.env.outputs.GCP_PROJECT_ID }}/${{ steps.env.outputs.ARTIFACT_REGISTRY_REPO }}/lol-highlight-backend:main-$(git rev-parse --short HEAD)"
echo "tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
echo "Using image: $IMAGE_TAG"
- name: Deploy to Compute Engine with zero-downtime
id: deploy
env:
INSTANCE_NAME: ${{ steps.env.outputs.COMPUTE_INSTANCE_NAME }}
ZONE: ${{ steps.env.outputs.COMPUTE_ZONE }}
IMAGE_TAG: ${{ steps.image.outputs.tag }}
GCP_REGION: ${{ steps.env.outputs.GCP_REGION }}
run: |
echo "${{ secrets.PROD_ENV_FILE }}" > .env.tmp
gcloud compute scp .env.tmp $INSTANCE_NAME:/tmp/.env --zone=$ZONE --tunnel-through-iap
rm -f .env.tmp
gcloud compute ssh $INSTANCE_NAME --zone=$ZONE --tunnel-through-iap --command="
set -e
export IMAGE_TAG='$IMAGE_TAG'
export GCP_REGION='$GCP_REGION'
echo '==> Step 1: Docker 인증'
gcloud auth configure-docker \$GCP_REGION-docker.pkg.dev
echo '==> Step 2: 현재 실행 중인 이미지 저장 (롤백용)'
PREVIOUS_IMAGE=\$(docker inspect nexus-spring-container --format='{{.Config.Image}}' 2>/dev/null || echo 'none')
echo \"Previous image: \$PREVIOUS_IMAGE\"
echo '==> Step 3: 새 이미지 Pull'
if ! docker pull \$IMAGE_TAG; then
echo 'Failed to pull new image'
exit 1
fi
echo '==> Step 4: 새 컨테이너 시작 (임시 포트 8081)'
docker run -d \
--name nexus-spring-container-new \
--restart unless-stopped \
-p 8081:8080 \
--env-file /tmp/.env \
\$IMAGE_TAG
echo '==> Step 5: 새 컨테이너 헬스체크 (최대 30초)'
HEALTHY=false
for i in {1..15}; do
if curl -f http://localhost:8081/health 2>/dev/null; then
HEALTHY=true
echo \"Health check passed after \$i attempts\"
break
fi
echo \"Health check attempt \$i/15...\"
sleep 2
done
if [ \"\$HEALTHY\" = true ]; then
echo '==> Step 6: 헬스체크 성공, 기존 컨테이너 교체'
docker stop nexus-spring-container || true
docker rm nexus-spring-container || true
docker stop nexus-spring-container-new
docker rm nexus-spring-container-new
docker run -d \
--name nexus-spring-container \
--restart unless-stopped \
-p 8080:8080 \
--env-file /tmp/.env \
\$IMAGE_TAG
echo '==> Step 7: 최종 확인'
sleep 3
if curl -f http://localhost:8080/health 2>/dev/null; then
echo 'Deployment successful!'
else
echo 'Warning: Final health check failed, but container is running'
fi
docker images --format '{{.Repository}}:{{.Tag}}' | \
grep 'lol-highlight-backend' | \
tail -n +4 | \
xargs -r docker rmi 2>/dev/null || true
else
echo '==> Rollback: 헬스체크 실패, 새 컨테이너 제거'
docker stop nexus-spring-container-new || true
docker rm nexus-spring-container-new || true
docker rmi \$IMAGE_TAG || true
echo 'Rollback completed. Previous container still running.'
exit 1
fi
rm -f /tmp/.env
echo 'Deployment completed successfully'
"
- name: Wait for load balancer
run: sleep 5
- name: Health check via Load Balancer
run: |
LOAD_BALANCER_IP="${{ secrets.LOAD_BALANCER_IP }}"
echo "Health check via Load Balancer..."
for i in {1..10}; do
if curl -f -m 5 http://${LOAD_BALANCER_IP}/health 2>/dev/null; then
echo "Health check passed!"
exit 0
fi
echo "Attempt $i/10..."
sleep 3
done
echo "Health check failed after 10 attempts"
exit 1
- name: Deployment success
if: success()
run: |
echo "Deployment completed successfully!"
echo "Image: ${{ steps.image.outputs.tag }}"
echo "Time: $(date)"
- name: Deployment failure
if: failure()
run: |
echo "Deployment failed!"
echo "Image: ${{ steps.image.outputs.tag }}"
echo "Previous container should still be running."