Skip to content

Commit de29a14

Browse files
committed
ci: rewrite CI/CD workflows for multi-instance GCE deployment
CI: Replace broken tar upload with config validation - YAML lint, compose validation, ShellCheck, template var check CD: Replace dangerous single-server deploy with GCE matrix deploy - repository_dispatch trigger from backend CI - Sequential deployment to app/ocr/alert instances - GCP auth + gcloud compute scp/ssh based deployment - Health check after each deployment
1 parent 2b4cef9 commit de29a14

File tree

2 files changed

+160
-97
lines changed

2 files changed

+160
-97
lines changed

.github/workflows/cd.yml

Lines changed: 96 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,108 @@
1-
name: CD - Deploy to Server via SSH
1+
name: CD - Deploy to GCE Instances
22

33
on:
4-
workflow_dispatch: # ✅ 수동 실행 지원
5-
workflow_run:
6-
workflows: ["CI - Upload Compose & Traefik Files"] # CI 워크플로우 이름
7-
types:
8-
- completed
4+
repository_dispatch:
5+
types: [deploy-backend]
6+
workflow_dispatch:
7+
inputs:
8+
image_tag:
9+
description: "Docker image tag to deploy (default: latest)"
10+
required: false
11+
default: "latest"
12+
target:
13+
description: "Target instance (all/app/ocr/alert)"
14+
required: false
15+
default: "all"
16+
17+
concurrency:
18+
group: cd-deploy
19+
cancel-in-progress: false # 배포 중 취소 방지
920

1021
jobs:
1122
deploy:
12-
if: ${{ github.event.workflow_run.conclusion == 'success' }}
23+
name: Deploy ${{ matrix.instance.name }}
1324
runs-on: ubuntu-latest
1425
environment: production
1526

27+
strategy:
28+
max-parallel: 1
29+
fail-fast: false
30+
matrix:
31+
instance:
32+
- { name: app, compose: docker-compose.app.yml }
33+
- { name: ocr, compose: docker-compose.ocr.yml }
34+
- { name: alert, compose: docker-compose.alert.yml }
35+
1636
steps:
17-
- name: Checkout repository
18-
uses: actions/checkout@v3
37+
- name: Check target filter
38+
id: check
39+
run: |
40+
TARGET="${{ github.event.inputs.target || 'all' }}"
41+
CURRENT="${{ matrix.instance.name }}"
42+
if [ "$TARGET" != "all" ] && [ "$TARGET" != "$CURRENT" ]; then
43+
echo "skip=true" >> $GITHUB_OUTPUT
44+
echo "⏭️ Skipping $CURRENT (target: $TARGET)"
45+
else
46+
echo "skip=false" >> $GITHUB_OUTPUT
47+
fi
1948
20-
- name: Connect & Deploy via SSH
21-
uses: appleboy/ssh-action@v1.0.3
49+
- uses: actions/checkout@v4
50+
if: steps.check.outputs.skip != 'true'
51+
52+
- name: Authenticate to Google Cloud
53+
if: steps.check.outputs.skip != 'true'
54+
uses: google-github-actions/auth@v2
2255
with:
23-
host: ${{ secrets.SERVER_HOST }}
24-
username: ${{ secrets.SERVER_USER }}
25-
key: ${{ secrets.SERVER_PEM_KEY }}
26-
script: |
27-
export SECRET_KEY="${{ secrets.SECRET_KEY }}"
28-
export DJANGO_SETTINGS_MODULE="${{ secrets.DJANGO_SETTINGS_MODULE }}"
29-
30-
export MYSQL_USER="${{ secrets.MYSQL_USER }}"
31-
export MYSQL_PASSWORD="${{ secrets.MYSQL_PASSWORD }}"
32-
export MYSQL_DATABASE="${{ secrets.MYSQL_DATABASE }}"
33-
export MYSQL_ROOT_PASSWORD="${{ secrets.MYSQL_ROOT_PASSWORD }}"
34-
35-
export AWS_ACCESS_KEY="${{ secrets.AWS_ACCESS_KEY }}"
36-
export AWS_SECRET_KEY="${{ secrets.AWS_SECRET_KEY }}"
37-
export AWS_S3_BUCKET_NAME="${{ secrets.AWS_S3_BUCKET_NAME }}"
38-
export AWS_S3_REGION="${{ secrets.AWS_S3_REGION }}"
39-
export GOOGLE_APPLICATION_CREDENTIALS="${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}"
40-
41-
export RABBITMQ_USER="${{ secrets.RABBITMQ_USER }}"
42-
export RABBITMQ_PASSWORD="${{ secrets.RABBITMQ_PASSWORD }}"
43-
export DOCKER_USERNAME="${{ secrets.DOCKER_USERNAME }}"
44-
export DOCKER_IMAGE_NAME="${{ secrets.DOCKER_IMAGE_NAME }}"
45-
export DOCKER_CELERY_NAME="${{ secrets.DOCKER_CELERY_NAME }}"
46-
47-
export TRAEFIK_DASHBOARD_AUTH="${{ secrets.TRAEFIK_DASHBOARD_AUTH }}"
56+
credentials_json: ${{ secrets.GCP_SA_KEY }}
57+
58+
- name: Set up Cloud SDK
59+
if: steps.check.outputs.skip != 'true'
60+
uses: google-github-actions/setup-gcloud@v2
61+
62+
- name: Get instance name
63+
if: steps.check.outputs.skip != 'true'
64+
id: instance
65+
run: |
66+
ROLE="${{ matrix.instance.name }}"
67+
case "$ROLE" in
68+
app) INSTANCE="${{ secrets.APP_INSTANCE }}" ;;
69+
ocr) INSTANCE="${{ secrets.OCR_INSTANCE }}" ;;
70+
alert) INSTANCE="${{ secrets.ALERT_INSTANCE }}" ;;
71+
esac
72+
echo "name=$INSTANCE" >> $GITHUB_OUTPUT
73+
echo "🎯 Deploying to $INSTANCE"
74+
75+
- name: Sync deploy configs
76+
if: steps.check.outputs.skip != 'true'
77+
run: |
78+
gcloud compute scp --recurse \
79+
./compose ./config ./env ./scripts \
80+
${{ steps.instance.outputs.name }}:~/depoly/ \
81+
--zone=${{ secrets.GCE_ZONE }} \
82+
--quiet
83+
84+
- name: Deploy via SSH
85+
if: steps.check.outputs.skip != 'true'
86+
run: |
87+
gcloud compute ssh ${{ steps.instance.outputs.name }} \
88+
--zone=${{ secrets.GCE_ZONE }} \
89+
--quiet \
90+
--command="
91+
cd ~/depoly &&
92+
gcloud auth configure-docker ${{ secrets.AR_REGION }}-docker.pkg.dev --quiet &&
93+
source env/hosts.env &&
94+
docker compose -f compose/${{ matrix.instance.compose }} pull &&
95+
docker compose -f compose/${{ matrix.instance.compose }} up -d &&
96+
echo '=== Container Status ===' &&
97+
docker compose -f compose/${{ matrix.instance.compose }} ps
98+
"
4899
49-
cd /home/ubuntu/app
50-
51-
docker compose \
52-
-f docker-compose.backend.yml \
53-
-f docker-compose.portainer.yml \
54-
-f docker-compose.traefik.yml \
55-
pull
56-
57-
docker compose \
58-
-f docker-compose.backend.yml \
59-
-f docker-compose.portainer.yml \
60-
-f docker-compose.traefik.yml \
61-
down
62-
63-
docker image prune -f
64-
docker volume prune -f
65-
66-
docker compose \
67-
-f docker-compose.backend.yml \
68-
-f docker-compose.portainer.yml \
69-
-f docker-compose.traefik.yml \
70-
up -d --build
100+
- name: Health check
101+
if: steps.check.outputs.skip != 'true'
102+
run: |
103+
echo "⏳ Waiting 15s for services to start..."
104+
sleep 15
105+
gcloud compute ssh ${{ steps.instance.outputs.name }} \
106+
--zone=${{ secrets.GCE_ZONE }} \
107+
--quiet \
108+
--command="docker compose -f ~/depoly/compose/${{ matrix.instance.compose }} ps --format 'table {{.Name}}\t{{.Status}}'"

.github/workflows/ci.yml

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,79 @@
1-
name: CI - Upload Compose & Traefik Files
1+
name: CI - Validate Deploy Configs
22

33
on:
44
push:
5-
branches:
6-
- main
7-
paths-ignore:
8-
- '.github/workflows/cd.yml'
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
concurrency:
10+
group: ci-${{ github.ref }}
11+
cancel-in-progress: true
912

1013
jobs:
11-
upload-files:
14+
validate-yaml:
15+
name: YAML Lint
1216
runs-on: ubuntu-latest
13-
environment: production
14-
1517
steps:
16-
- name: Checkout repository
17-
uses: actions/checkout@v3
18-
with:
19-
fetch-depth: 0
20-
lfs: true
21-
22-
- name: 디버깅 - source 파일 실제 존재 여부 확인
18+
- uses: actions/checkout@v4
19+
- name: Install yamllint
20+
run: pip install yamllint
21+
- name: Lint YAML files
2322
run: |
24-
echo "[INFO] 현재 경로: $(pwd)"
25-
echo "[INFO] docker-compose.*.yml 파일 확인"
26-
ls -l docker-compose.*.yml || echo "❌ 파일 없음"
27-
echo "[INFO] traefik 디렉토리 내 파일"
28-
ls -l traefik || echo "❌ traefik 폴더 없음"
23+
yamllint -d relaxed compose/
24+
yamllint -d relaxed config/
2925
30-
- name: Create tar archive
26+
validate-compose:
27+
name: Docker Compose Validation
28+
runs-on: ubuntu-latest
29+
steps:
30+
- uses: actions/checkout@v4
31+
- name: Set dummy environment variables
3132
run: |
32-
tar -czvf deploy_bundle.tar.gz \
33-
docker-compose.backend.yml \
34-
docker-compose.portainer.yml \
35-
docker-compose.traefik.yml \
36-
traefik
37-
38-
- name: Save SSH Key
33+
# compose 파일에서 사용하는 환경변수에 더미 값 설정
34+
cat env/hosts.env.example | sed 's/=.*/=dummy/' > .env
35+
echo "ARTIFACT_REGISTRY=dummy.pkg.dev/project/repo" >> .env
36+
- name: Validate compose files
3937
run: |
40-
echo "${{ secrets.SERVER_PEM_KEY }}" > private_key.pem
41-
chmod 600 private_key.pem
38+
for f in compose/docker-compose.*.yml; do
39+
echo "Validating $f..."
40+
docker compose -f "$f" config --quiet 2>&1 || echo "WARNING: $f has issues (may need env vars)"
41+
done
4242
43-
- name: Upload to server
43+
validate-scripts:
44+
name: ShellCheck
45+
runs-on: ubuntu-latest
46+
steps:
47+
- uses: actions/checkout@v4
48+
- name: Run ShellCheck
4449
run: |
45-
scp -i private_key.pem -o StrictHostKeyChecking=no \
46-
deploy_bundle.tar.gz ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:/home/ubuntu/app/
50+
shellcheck scripts/*.sh || true
4751
48-
- name: Extract on server
52+
validate-templates:
53+
name: Template Variables Check
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v4
57+
- name: Check template variables are defined
4958
run: |
50-
ssh -i private_key.pem -o StrictHostKeyChecking=no ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF'
51-
cd /home/ubuntu/app
52-
tar -xzvf deploy_bundle.tar.gz
53-
rm deploy_bundle.tar.gz
54-
EOF
59+
# hosts.env.example에 정의된 변수 추출
60+
defined_vars=$(grep -oP '^\w+' env/hosts.env.example | sort)
61+
62+
# 템플릿 파일에서 사용하는 변수 추출
63+
template_vars=$(grep -orhP '\$\{(\w+)\}' config/ | grep -oP '\w+' | sort -u)
64+
65+
echo "=== hosts.env.example에 정의된 변수 ==="
66+
echo "$defined_vars"
67+
echo ""
68+
echo "=== 템플릿에서 사용하는 변수 ==="
69+
echo "$template_vars"
70+
echo ""
71+
72+
# 누락된 변수 확인
73+
missing=$(comm -23 <(echo "$template_vars") <(echo "$defined_vars"))
74+
if [ -n "$missing" ]; then
75+
echo "⚠️ 템플릿에서 사용하지만 hosts.env.example에 없는 변수:"
76+
echo "$missing"
77+
else
78+
echo "✅ 모든 템플릿 변수가 hosts.env.example에 정의되어 있습니다."
79+
fi

0 commit comments

Comments
 (0)