Skip to content

Commit d1589e9

Browse files
committed
hotfix: Pull-it 배포 사전검증과 롤백을 보장한다
1 parent 3ce807e commit d1589e9

4 files changed

Lines changed: 195 additions & 136 deletions

File tree

.env.example

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ PULLIT_DB_VOLUME=pullit-prod-db-data
1414
PULLIT_REDIS_VOLUME=pullit-prod-redis-data
1515
PULLIT_RABBITMQ_VOLUME=pullit-prod-rabbitmq-data
1616
RABBITMQ_DEFAULT_USER=pullit_app
17-
S3_BUCKET_NAME=pullit-portfolio-production-52685a79
18-
S3_REGION=ap-northeast-2
17+
S3_BUCKET_NAME=pullit-portfolio-assets
18+
S3_REGION=us-east-1
1919
KAKAO_REDIRECT_URI=https://portfolio.yeon.world/pull-it/login/oauth2/code/kakao
2020
JWT_ISSUER=pullit-backend
2121
JWT_AUDIENCE=pullit-client
@@ -35,6 +35,7 @@ JWT_SECRET_KEY=__SECRET__
3535
KAKAO_REST_API_KEY=__SECRET__
3636
KAKAO_CLIENT_SECRET=__SECRET__
3737
GEMINI_API_KEY=__SECRET__
38-
# 운영은 EC2 instance profile의 최소 권한으로 접근한다. 로컬 개발에서만 필요하면 별도 .env에 넣는다.
38+
# Pi의 Pull-it 전용 MinIO root credentials. 기존 Yeon/AWS credential을 재사용하지 않는다.
39+
S3_ACCESS_KEY=__SECRET__
40+
S3_SECRET_KEY=__SECRET__
3941
SENTRY_DSN=__SECRET__
40-
PULLIT_CLOUDFLARE_TUNNEL_TOKEN=__SECRET__

.github/workflows/cd.yml

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ name: Deploy Pull-it backend and worker to Yeon Pi
22

33
on:
44
workflow_dispatch:
5+
inputs:
6+
operation:
7+
description: 'preflight은 Pi에 쓰지 않고 전송 경로만 검증합니다.'
8+
required: true
9+
default: preflight
10+
type: choice
11+
options: [preflight, deploy]
512
push:
613
branches: [develop]
714
paths:
@@ -23,7 +30,63 @@ concurrency:
2330
cancel-in-progress: false
2431

2532
jobs:
33+
transport-preflight:
34+
runs-on: ubuntu-latest
35+
environment: pullit-backend-production
36+
steps:
37+
- name: Install pinned Cloudflare Access client
38+
run: |
39+
set -euo pipefail
40+
curl --fail --location --retry 3 --output /tmp/cloudflared \
41+
https://github.com/cloudflare/cloudflared/releases/download/2026.5.0/cloudflared-linux-amd64
42+
echo '0095e46fdc88855d801c4d304cb1f5dd4bd656116c47ab94c2ad0ae7cda1c7ec /tmp/cloudflared' | sha256sum --check -
43+
install -D -m 0755 /tmp/cloudflared "$HOME/.local/bin/cloudflared"
44+
printf '%s\n' "$HOME/.local/bin" >> "$GITHUB_PATH"
45+
- name: Verify Cloudflare Access and the restricted Pi command path
46+
env:
47+
DEPLOY_KEY: ${{ secrets.PULLIT_DEPLOY_SSH_PRIVATE_KEY }}
48+
KNOWN_HOSTS: ${{ vars.PULLIT_DEPLOY_SSH_KNOWN_HOSTS }}
49+
DEPLOY_HOST: ${{ vars.PULLIT_DEPLOY_HOST }}
50+
PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID: ${{ secrets.PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID }}
51+
PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET: ${{ secrets.PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET }}
52+
DB_USERNAME: ${{ vars.PULLIT_DB_USERNAME }}
53+
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
54+
DB_ROOT_PASSWORD: ${{ secrets.DB_ROOT_PASSWORD }}
55+
PULLIT_DATABASE_NAME: ${{ vars.PULLIT_DATABASE_NAME }}
56+
RABBITMQ_DEFAULT_USER: ${{ vars.RABBITMQ_DEFAULT_USER }}
57+
RABBITMQ_DEFAULT_PASS: ${{ secrets.RABBITMQ_DEFAULT_PASS }}
58+
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
59+
KAKAO_REST_API_KEY: ${{ secrets.KAKAO_REST_API_KEY }}
60+
KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }}
61+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
62+
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
63+
S3_REGION: ${{ vars.S3_REGION }}
64+
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
65+
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
66+
run: |
67+
set -euo pipefail
68+
required=(DEPLOY_KEY KNOWN_HOSTS DEPLOY_HOST PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET DB_USERNAME DB_PASSWORD DB_ROOT_PASSWORD PULLIT_DATABASE_NAME RABBITMQ_DEFAULT_USER RABBITMQ_DEFAULT_PASS JWT_SECRET_KEY KAKAO_REST_API_KEY KAKAO_CLIENT_SECRET GEMINI_API_KEY S3_BUCKET_NAME S3_REGION S3_ACCESS_KEY S3_SECRET_KEY)
69+
for name in "${required[@]}"; do test -n "${!name}"; done
70+
case "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID" in 'CF-Access-Client-Id:'*) exit 1 ;; esac
71+
case "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET" in 'CF-Access-Client-Secret:'*) exit 1 ;; esac
72+
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
73+
--header "CF-Access-Client-Id: $PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID" \
74+
--header "CF-Access-Client-Secret: $PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET" "https://$DEPLOY_HOST")"
75+
test "$status" = 200 || { echo "Cloudflare Access service-token probe returned HTTP $status" >&2; exit 1; }
76+
install -d -m 700 ~/.ssh
77+
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519
78+
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
79+
chmod 600 ~/.ssh/id_ed25519
80+
printf 'Host %s\n' "$DEPLOY_HOST" > ~/.ssh/config
81+
cat >> ~/.ssh/config <<'EOF'
82+
ProxyCommand cloudflared access ssh --hostname %h --service-token-id "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID" --service-token-secret "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET"
83+
EOF
84+
chmod 600 ~/.ssh/config
85+
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes "pullit-deploy@$DEPLOY_HOST" \
86+
'test -d /opt/pullit/incoming && test -w /opt/pullit/incoming && sudo -n -l /usr/local/sbin/pullit-deploy >/dev/null'
2687
build:
88+
needs: transport-preflight
89+
if: github.event_name == 'push' || inputs.operation == 'deploy'
2790
runs-on: ubuntu-latest
2891
steps:
2992
- uses: actions/checkout@v5
@@ -43,7 +106,8 @@ jobs:
43106
labels: org.opencontainers.image.source=https://github.com/${{ github.repository }}
44107

45108
deploy:
46-
needs: build
109+
needs: [transport-preflight, build]
110+
if: github.event_name == 'push' || inputs.operation == 'deploy'
47111
runs-on: ubuntu-latest
48112
environment: pullit-backend-production
49113
steps:
@@ -56,27 +120,6 @@ jobs:
56120
echo '0095e46fdc88855d801c4d304cb1f5dd4bd656116c47ab94c2ad0ae7cda1c7ec /tmp/cloudflared' | sha256sum --check -
57121
install -D -m 0755 /tmp/cloudflared "$HOME/.local/bin/cloudflared"
58122
printf '%s\n' "$HOME/.local/bin" >> "$GITHUB_PATH"
59-
- name: Verify Cloudflare Access service token
60-
env:
61-
DEPLOY_HOST: ${{ vars.PULLIT_DEPLOY_HOST }}
62-
PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID: ${{ secrets.PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID }}
63-
PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET: ${{ secrets.PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET }}
64-
run: |
65-
set -euo pipefail
66-
test -n "$DEPLOY_HOST"
67-
test -n "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID"
68-
test -n "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET"
69-
case "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID" in
70-
'CF-Access-Client-Id:'*) echo 'Cloudflare client ID must be the value only.' >&2; exit 1 ;;
71-
esac
72-
case "$PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET" in
73-
'CF-Access-Client-Secret:'*) echo 'Cloudflare client secret must be the value only.' >&2; exit 1 ;;
74-
esac
75-
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
76-
--header "CF-Access-Client-Id: $PULLIT_CLOUDFLARE_ACCESS_CLIENT_ID" \
77-
--header "CF-Access-Client-Secret: $PULLIT_CLOUDFLARE_ACCESS_CLIENT_SECRET" \
78-
"https://$DEPLOY_HOST")"
79-
test "$status" = 200 || { echo "Cloudflare Access service-token probe returned HTTP $status" >&2; exit 1; }
80123
- name: Create isolated runtime environment and deploy it
81124
env:
82125
PULLIT_BACKEND_IMAGE: ghcr.io/hyeonjun0527/pullit-backend:${{ github.sha }}

0 commit comments

Comments
 (0)