클라우드 3단계: CD(지속적 배포) 파이프라인 구축 - 100-hours-a-week/16-Hot6-wiki GitHub Wiki
상위 문서: 클라우드 위키
관련 문서: 클라우드 WHY? 문서
- CD 적용 범위 및 방식 결정
dev에는 자동 CI/CD (deployment), prod에는 수동으로 deploy를 진행합니다(delivery)
아래 위키 문서에서 왜 각각을 그렇게 구분했는지 상세히 작성했습니다.
왜 이 dev 구조인가요?
왜 이 prod 구조인가요?
- CD 아키텍처 설계
왜 VPN을 사용했나요?
왜 WireGuard VPN을 사용했나요?
CD 아키텍처는 위와 같습니다. 아래 스크립트들은 구체적인 아키텍처인 MVP 버전 아키텍처에 맞추어 작성되었습니다.
✅ Dev 환경 Deployment 흐름도 | ✅ Prod 환경 Delivery 시나리오 |
---|---|
![]() |
![]() |
0. Github Actions 아티펙트 준비 완료
1. Cloud Engineer 배포 시작 트리거
2-1. FE 정적 자산 S3 업로드 (v2)
2-2. BE/AI 서버 인스턴스 준비 (v2)
3. v1 서버로 오는 요청은 Nginx가 HTTP 상태 코드 503 응답(배포 중이라는 의미)
4. CloudFront 오리진 전환 v1 → v2
5. CloudFront 캐시 무효화
6. LB 타겟 그룹에 v2로 전환
7. 수동 테스트
8-1. 테스트 통과: v1 제거 및 배포 완료
8-2. 테스트 실패: v1 으로 롤백
0. Github Actions 아티펙트 준비 완료
1. 정적 파일 S3 업로드
2. CloudFront 캐시 무효화
0. Github Actions 아티펙트 준비 완료
1. Shared 서버 SSH 접속
2. 개발 서버 SSH 접속
3. 아티펙트 다운로드
4. 서버 재시작(Systemctl)
5. 배포 완료 / 실패 시 알람 전송
[Unit]
Description=Spring Boot Backend Service
After=network.target
[Service]
User=ec2-user
ExecStart=/usr/bin/java -jar /home/ec2-user/artifacts/backend-latest.jar
SuccessExitStatus=143
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
[Unit]
Description=FastAPI AI Service
After=network.target
[Service]
User=ec2-user
WorkingDirectory=/home/ec2-user/onthetop-ai
ExecStart=/home/ec2-user/onthetop-ai/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
# Spring
sudo systemctl restart backend.service
# FastAPI
sudo systemctl restart ai.service
name: Blue-Green Deploy to Production
on:
workflow_dispatch:
jobs:
blue-green-deploy:
name: Blue-Green Deploy
runs-on: ubuntu-latest
env:
PROJECT_ID: ${{ secrets.GCP_PROJECT }}
REGION: asia-northeast3
ZONE: asia-northeast3-a
BACKEND_SERVICE: my-backend-service
S3_BUCKET: ${{ secrets.S3_BUCKET_NAME }}
steps:
- name: Checkout Infra Repo
uses: actions/checkout@v3
- name: Auth to GCP
uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Setup gcloud CLI
uses: google-github-actions/setup-gcloud@v1
with:
project_id: ${{ env.PROJECT_ID }}
- name: Auth to AWS (for CloudFront)
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-2
- name: Determine Current Target Group
id: determine-group
run: |
CURRENT_GROUP=$(gcloud compute backend-services describe "$BACKEND_SERVICE" \
--global \
--format="value(backends[].group)")
if echo "$CURRENT_GROUP" | grep -q "green"; then
echo "NEXT_GROUP=backend-blue-group" >> $GITHUB_ENV
echo "NEXT_TEMPLATE=backend-blue-template" >> $GITHUB_ENV
echo "PREV_GROUP=backend-green-group" >> $GITHUB_ENV
else
echo "NEXT_GROUP=backend-green-group" >> $GITHUB_ENV
echo "NEXT_TEMPLATE=backend-green-template" >> $GITHUB_ENV
echo "PREV_GROUP=backend-blue-group" >> $GITHUB_ENV
fi
- name: Upload new FE static files to S3
run: |
aws s3 sync ./frontend/dist s3://$S3_BUCKET/v2 --delete
- name: Create Instance Template for ${{ env.NEXT_GROUP }}
run: |
gcloud compute instance-templates create "$NEXT_TEMPLATE" \
--machine-type=e2-medium \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--metadata=startup-script='#! /bin/bash
cd /home/ec2-user
wget https://example.com/backend-latest.jar -O backend.jar
nohup java -jar backend.jar --spring.profiles.active=prod > app.log 2>&1 &' \
--tags=http-server \
--region=$REGION
- name: Create New Instance Group for ${{ env.NEXT_GROUP }}
run: |
gcloud compute instance-groups managed create "$NEXT_GROUP" \
--base-instance-name="$NEXT_GROUP" \
--size=2 \
--template="$NEXT_TEMPLATE" \
--zone=$ZONE
- name: Wait for Instances to Start
run: |
echo "⏳ Waiting 90s for app to boot..."
sleep 90
- name: SSH into Jump Server and Deploy Backend + AI
run: |
JUMP_HOST=your-jump-host
TARGET_HOST=your-target-server
echo "SSH to Jump Server ($JUMP_HOST)"
ssh -o StrictHostKeyChecking=no ec2-user@$JUMP_HOST << 'EOS'
echo "Cloning Cloud Repository..."
if [ -d "cloud-repo" ]; then
cd infra-repo && git pull
else
git clone https://github.com/your-org/your-repo.git cloud-repo
cd cloud-repo
fi
echo "🚚 Copying Scripts to Target Host ($TARGET_HOST)..."
scp -o StrictHostKeyChecking=no ./scripts/deploy-backend.sh ec2-user@$TARGET_HOST:/home/ec2-user/deploy-backend.sh
scp -o StrictHostKeyChecking=no ./scripts/deploy-ai.sh ec2-user@$TARGET_HOST:/home/ec2-user/deploy-ai.sh
echo "🚀 Executing Scripts on Target Host..."
ssh -o StrictHostKeyChecking=no ec2-user@$TARGET_HOST << 'INNER'
chmod +x deploy-backend.sh
chmod +x deploy-ai.sh
sudo ./deploy-backend.sh
sudo ./deploy-ai.sh
INNER
EOS
# 이곳에 v1 503 전환 로직 추가
- name: Get Current CloudFront Config
id: get-config
run: |
aws cloudfront get-distribution-config \
--id ${{ secrets.CLOUDFRONT_ID }} > dist-config.json
echo "etag=$(jq -r '.ETag' dist-config.json)" >> $GITHUB_OUTPUT
echo "origin=$(jq -r '.DistributionConfig.DefaultCacheBehavior.TargetOriginId' dist-config.json)" >> $GITHUB_OUTPUT
- name: Determine Next Origin
id: determine-origin
run: |
if [ "${{ steps.get-config.outputs.origin }}" == "s3-blue" ]; then
echo "NEXT_ORIGIN=s3-green" >> $GITHUB_ENV
else
echo "NEXT_ORIGIN=s3-blue" >> $GITHUB_ENV
fi
- name: Switch CloudFront Origin
run: |
jq --arg origin "$NEXT_ORIGIN" \
'.DistributionConfig.DefaultCacheBehavior.TargetOriginId = $origin' dist-config.json > new-config.json
aws cloudfront update-distribution \
--id ${{ secrets.CLOUDFRONT_ID }} \
--distribution-config file://new-config.json \
--if-match "${{ steps.get-config.outputs.etag }}"
- name: Invalidate CloudFront Cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_ID }} \
--paths "/*"
- name: Switch LB to ${{ env.NEXT_GROUP }}
run: |
gcloud compute backend-services update "$BACKEND_SERVICE" \
--global \
--instance-groups="$NEXT_GROUP" \
--instance-group-zone=$ZONE
- name: Manual Verification
run: |
echo "✅ Please verify deployment at https://onthe-top.com"
echo "🧹 Once verified, run apply workflow to delete previous group: $PREV_GROUP"
name: Finalize Deployment (Delete v1)
on:
workflow_dispatch:
inputs:
group_to_delete:
description: "Enter the instance group to delete (e.g., backend-blue-group)"
required: true
jobs:
delete-old:
name: Remove Previous Version (v1)
runs-on: ubuntu-latest
steps:
- name: Delete old instance group
run: |
gcloud compute instance-groups managed delete ${{ github.event.inputs.group_to_delete }} \
--zone ${{ env.REGION }}-a --quiet
- name: Notify
run: echo "🧹 이전 인스턴스 그룹 (${{ github.event.inputs.group_to_delete }}) 삭제 완료. 배포 최종 완료."
name: Rollback Deployment
on:
workflow_dispatch:
inputs:
rollback_target_group:
description: "복구할 인스턴스 그룹 이름 (예: backend-blue-group)"
required: true
delete_group:
description: "삭제할 신규 인스턴스 그룹 이름 (예: backend-green-group)"
required: true
jobs:
rollback:
name: Rollback to Previous Version
runs-on: ubuntu-latest
env:
PROJECT_ID: ${{ secrets.GCP_PROJECT }}
REGION: asia-northeast3
ZONE: asia-northeast3-a
BACKEND_SERVICE: my-backend-service
steps:
- name: Auth to GCP
uses: google-github-actions/auth@v1
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@v1
with:
project_id: ${{ env.PROJECT_ID }}
- name: 🔁 Revert LB to ${{ github.event.inputs.rollback_target_group }}
run: |
gcloud compute backend-services update "$BACKEND_SERVICE" \
--global \
--instance-groups=${{ github.event.inputs.rollback_target_group }} \
--instance-group-zone=${{ env.ZONE }}
- name: ❌ Delete broken group ${{ github.event.inputs.delete_group }}
run: |
gcloud compute instance-groups managed delete ${{ github.event.inputs.delete_group }} \
--zone ${{ env.ZONE }} --quiet
- name: ✅ Notify
run: |
echo "🔁 롤백 완료: LB는 ${{ github.event.inputs.rollback_target_group }}로 복구되었습니다."
echo "🗑️ 실패한 그룹(${{ github.event.inputs.delete_group }})은 삭제되었습니다."
name: Deploy FE to Dev
on:
push:
branches:
- dev
jobs:
deploy-fe:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v3
- name: Build Frontend
run: |
cd frontend
npm install
npm run build
- name: Upload to S3
run: |
aws s3 sync ./frontend/dist s3://${{ secrets.S3_BUCKET_DEV }} --delete
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-northeast-2
- name: Invalidate CloudFront (optional)
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_ID_DEV }} \
--paths "/*"
name: Deploy BE to Dev
on:
push:
branches:
- dev
jobs:
deploy-be:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v3
- name: Build Spring Boot JAR
run: |
cd backend
./gradlew clean build -x test
- name: Copy Artifact via SSH
run: |
scp -o StrictHostKeyChecking=no backend/build/libs/*.jar ec2-user@${{ secrets.DEV_SERVER_IP }}:/home/ec2-user/artifacts/backend-latest.jar
- name: Restart Backend Service
run: |
ssh -o StrictHostKeyChecking=no ec2-user@${{ secrets.DEV_SERVER_IP }} \
"sudo systemctl restart backend.service"
name: Deploy AI to Dev
on:
push:
branches:
- dev
jobs:
deploy-ai:
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v3
- name: Zip AI Source
run: |
cd ai
zip -r ai.zip .
- name: Transfer Code via SCP
run: |
scp -o StrictHostKeyChecking=no ai/ai.zip ec2-user@${{ secrets.DEV_SERVER_IP }}:/home/ec2-user/ai.zip
- name: Deploy and Restart AI
run: |
ssh -o StrictHostKeyChecking=no ec2-user@${{ secrets.DEV_SERVER_IP }} << 'EOS'
unzip -o /home/ec2-user/ai.zip -d /home/ec2-user/onthetop-ai
sudo systemctl restart ai.service
EOS