Skip to content

Release: v1.0.0-beta.2 (#316) (#325) #120

Release: v1.0.0-beta.2 (#316) (#325)

Release: v1.0.0-beta.2 (#316) (#325) #120

Workflow file for this run

name: CD (Deploy to GCP - Dev)
on:
push:
branches: ['dev']
permissions:
contents: read
id-token: write
env:
DOCKER_IMAGE: asia-northeast3-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/folioo-docker/folioo-server
concurrency:
group: deploy-dev
cancel-in-progress: true
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
- name: Authenticate to Artifact Registry
run: gcloud auth configure-docker asia-northeast3-docker.pkg.dev
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and Push to Artifact Registry
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ env.DOCKER_IMAGE }}:dev
${{ env.DOCKER_IMAGE }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-dev:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Supabase CLI
uses: supabase/setup-cli@v1
with:
version: 2.75.0
- name: Apply Supabase migrations (dev)
run: |
if [ -z "${{ secrets.SUPABASE_DEV_DB_URL }}" ]; then
echo "::error::Missing required secret SUPABASE_DEV_DB_URL"
exit 1
fi
DB_URL="${{ secrets.SUPABASE_DEV_DB_URL }}"
if printf '%s' "$DB_URL" | grep -q ':6543'; then
echo "::error::SUPABASE_DEV_DB_URL points to transaction pooler port 6543."
echo "::error::Use a migration-safe connection string (direct DB or session-mode 5432)."
exit 1
fi
supabase db push --db-url "$DB_URL"
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
project_id: ${{ secrets.GCP_PROJECT_ID }}
- name: Load Deploy Config from Terraform
run: |
gcloud storage cp gs://${{ secrets.TF_STATE_BUCKET }}/deploy-config.json /tmp/deploy-config.json
echo "GCE_NAME=$(jq -r .dev_gce_name /tmp/deploy-config.json)" >> $GITHUB_ENV
echo "GCE_ZONE=$(jq -r .dev_gce_zone /tmp/deploy-config.json)" >> $GITHUB_ENV
- name: Refresh Environment Variables from Secret Manager
run: |
# 디렉토리 먼저 생성 (첫 배포 포함 모든 경우에 안전)
gcloud compute ssh ${{ env.GCE_NAME }} \
--zone=${{ env.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \
--tunnel-through-iap \
--quiet \
--command="sudo mkdir -p /home/folioo"
gcloud secrets versions access latest \
--project='${{ secrets.GCP_PROJECT_ID }}' \
--secret='folioo-dev-config' \
| jq -r 'to_entries[] | "\(.key | ascii_upcase)=\(.value)"' \
> .env.dev
gcloud compute scp .env.dev ${{ env.GCE_NAME }}:~/.env.dev \
--zone=${{ env.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \
--tunnel-through-iap \
--quiet
gcloud compute ssh ${{ env.GCE_NAME }} \
--zone=${{ env.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \
--tunnel-through-iap \
--quiet \
--command="
sudo mv ~/.env.dev /home/folioo/.env.dev
sudo chmod 600 /home/folioo/.env.dev
"
- name: Deploy to GCE via IAP (Blue-Green)
run: |
# B. compose 파일 전송
gcloud compute scp docker-compose.infra.yml docker-compose.dev.yml \
${{ env.GCE_NAME }}:~ \
--zone=${{ env.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \
--tunnel-through-iap \
--quiet
# C. Blue-Green 배포 실행
gcloud compute ssh ${{ env.GCE_NAME }} \
--zone=${{ env.GCE_ZONE }} \
--project=${{ secrets.GCP_PROJECT_ID }} \
--tunnel-through-iap \
--quiet \
--command="
sudo mv ~/docker-compose.infra.yml /home/folioo/docker-compose.infra.yml
sudo mv ~/docker-compose.dev.yml /home/folioo/docker-compose.dev.yml
cd /home/folioo
# jq 설치 확인 (없으면 설치)
if ! command -v jq &>/dev/null; then
sudo apt-get update -y -qq && sudo apt-get install -y -qq jq
fi
export DOCKER_IMAGE='${{ env.DOCKER_IMAGE }}'
# GCE 메타데이터 서버로 Docker 인증 (gcloud 불필요)
curl -sf 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' \
-H 'Metadata-Flavor: Google' \
| python3 -c 'import sys, json; print(json.load(sys.stdin)[\"access_token\"])' \
| docker login -u oauth2accesstoken --password-stdin https://asia-northeast3-docker.pkg.dev
DC='sudo -E docker compose --env-file .env.dev -f docker-compose.infra.yml -f docker-compose.dev.yml'
sudo docker network create folioo-network || true
# ── 현재 활성 슬롯 감지 ──
ACTIVE_SLOT=\$(\$DC ps --format json 2>/dev/null | jq -sr '.[].Service' 2>/dev/null | grep -E 'dev-blue|dev-green' | head -1 || true)
ACTIVE_SLOT=\${ACTIVE_SLOT:-none}
if [[ \"\$ACTIVE_SLOT\" == 'dev-blue' ]]; then
GREEN_SLOT='green'
BLUE_SLOT='blue'
else
GREEN_SLOT='blue'
BLUE_SLOT='green'
fi
echo \"Current: \$ACTIVE_SLOT / New slot: dev-\$GREEN_SLOT / Old: dev-\$BLUE_SLOT\"
# ── 최초 배포: 베이스 인프라 시작 ──
if [[ \"\$ACTIVE_SLOT\" == 'none' ]]; then
echo 'First deploy: starting base infra'
\$DC up -d traefik cloudflared
sleep 10
fi
\$DC --profile blue --profile green pull
echo \"Starting green slot (dev-\$GREEN_SLOT)\"
\$DC --profile \$GREEN_SLOT up -d --remove-orphans
# ── Health Check ──
GREEN_CONTAINER=\$(\$DC ps --format json 2>/dev/null | jq -sr \".[] | select(.Service == \\\"dev-\$GREEN_SLOT\\\") | .Name\")
echo \"Health check starting... (container: \$GREEN_CONTAINER)\"
for i in {1..36}; do
STATUS=\$(sudo docker inspect --format='{{.State.Health.Status}}' \"\$GREEN_CONTAINER\" 2>/dev/null || echo 'not_found')
if [[ \"\$STATUS\" == 'healthy' ]]; then
echo \"✅ Green slot healthy! (\${i}th check)\"
break
fi
if [[ \"\$STATUS\" == 'unhealthy' ]]; then
echo '❌ Container unhealthy - rolling back'
sudo docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' \"\$GREEN_CONTAINER\" 2>/dev/null || true
sudo docker logs --tail 80 \"\$GREEN_CONTAINER\" 2>/dev/null || true
sudo docker stop \"\$GREEN_CONTAINER\" 2>/dev/null && sudo docker rm -f \"\$GREEN_CONTAINER\" 2>/dev/null
exit 1
fi
if [ \$i -eq 36 ]; then
echo '❌ Health check timeout (180s) - rolling back'
sudo docker logs --tail 80 \"\$GREEN_CONTAINER\" 2>/dev/null || true
sudo docker stop \"\$GREEN_CONTAINER\" 2>/dev/null && sudo docker rm -f \"\$GREEN_CONTAINER\" 2>/dev/null
exit 1
fi
echo \"Waiting... (\$i/36) status=\$STATUS\"
sleep 5
done
# ── 구 슬롯 종료 (인프라는 유지) ──
if [[ \"\$ACTIVE_SLOT\" != 'none' ]]; then
OLD_CONTAINER=\$(\$DC ps --format json 2>/dev/null | jq -sr \".[] | select(.Service == \\\"dev-\$BLUE_SLOT\\\") | .Name\")
if [[ -n \"\$OLD_CONTAINER\" ]]; then
echo \"Stopping old slot (\$OLD_CONTAINER)...\"
sudo docker stop \"\$OLD_CONTAINER\" 2>/dev/null && sudo docker rm -f \"\$OLD_CONTAINER\" 2>/dev/null
fi
fi
sudo docker image prune -f
echo '✅ Dev deployment completed!'
"