-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy.sh
More file actions
executable file
·215 lines (179 loc) · 6.89 KB
/
Copy pathdeploy.sh
File metadata and controls
executable file
·215 lines (179 loc) · 6.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/bin/bash
set -euo pipefail
IMAGE="${1:-}"
if [ -z "$IMAGE" ]; then
echo ">>> 사용법: ./deploy.sh <이미지명:태그>"
exit 1
fi
APP_DIR="${APP_DIR:-/home/ubuntu/app}"
NETWORK="${NETWORK:-app-network}"
CADDY_CONTAINER="${CADDY_CONTAINER:-caddy}"
HEALTH_TIMEOUT_SECONDS="${HEALTH_TIMEOUT_SECONDS:-180}"
HEALTH_LOG_INTERVAL_SECONDS="${HEALTH_LOG_INTERVAL_SECONDS:-10}"
log() {
echo ">>> $*"
}
cleanup_new_container() {
if [ -n "${NEW_CONTAINER:-}" ]; then
docker stop "$NEW_CONTAINER" >/dev/null 2>&1 || true
docker rm "$NEW_CONTAINER" >/dev/null 2>&1 || true
fi
}
mkdir -p "$APP_DIR"
REQUIRED_ENV_VARS=(
DATABASE_HOST
DATABASE_USERNAME
DATABASE_PASSWORD
APP_JWT_SECRET
APP_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS
APP_JWT_REFRESH_TOKEN_EXPIRATION_SECONDS
)
MISSING_ENV_VARS=()
for ENV_VAR in "${REQUIRED_ENV_VARS[@]}"; do
if [ -z "${!ENV_VAR:-}" ]; then
MISSING_ENV_VARS+=("$ENV_VAR")
fi
done
if [ "${#MISSING_ENV_VARS[@]}" -gt 0 ]; then
echo ">>> 필수 운영 환경 변수가 없습니다: ${MISSING_ENV_VARS[*]}" >&2
echo ">>> GitHub Actions secrets 또는 환경 변수로 값을 설정한 뒤 다시 배포하세요." >&2
exit 1
fi
docker network inspect "$NETWORK" >/dev/null 2>&1 || docker network create "$NETWORK" >/dev/null
if ! docker ps --format '{{.Names}}' | grep -qx "$CADDY_CONTAINER"; then
echo ">>> Docker Caddy 컨테이너($CADDY_CONTAINER)가 실행 중이 아닙니다. scripts/setup-docker-caddy.sh를 먼저 실행하세요." >&2
exit 1
fi
CADDY_NETWORKS=$(docker inspect --format='{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}' "$CADDY_CONTAINER")
if ! echo "$CADDY_NETWORKS" | grep -qx "$NETWORK"; then
log "Caddy 컨테이너를 $NETWORK 네트워크에 연결"
docker network connect "$NETWORK" "$CADDY_CONTAINER"
fi
# 버전 기반 컨테이너 이름 (이미지 태그 추출)
VERSION=$(echo "$IMAGE" | cut -d':' -f2)
NEW_CONTAINER="app-${VERSION}"
# 현재 활성 컨테이너를 네트워크 alias로 탐색
CURRENT=""
for cid in $(docker ps -q 2>/dev/null); do
aliases=$(docker inspect "$cid" \
--format "{{range \$net, \$cfg := .NetworkSettings.Networks}}{{if eq \$net \"$NETWORK\"}}{{range \$cfg.Aliases}}{{.}} {{end}}{{end}}{{end}}" \
2>/dev/null)
if [[ " $aliases " == *" app "* ]]; then
CURRENT=$(docker inspect "$cid" --format '{{.Name}}' | sed 's/^\///')
break
fi
done
log "현재: ${CURRENT:-없음} → 새로운: $NEW_CONTAINER"
# ECR 이미지인 경우 credential helper로 인증 설정
if [[ "$IMAGE" == *".dkr.ecr."* ]]; then
ECR_REGISTRY=$(echo "$IMAGE" | cut -d'/' -f1)
if ! command -v docker-credential-ecr-login &>/dev/null; then
log "ECR credential helper 설치 중..."
sudo apt-get install -y amazon-ecr-credential-helper
fi
mkdir -p ~/.docker
echo "{\"credHelpers\":{\"${ECR_REGISTRY}\":\"ecr-login\"}}" > ~/.docker/config.json
log "ECR credential helper 설정 완료: $ECR_REGISTRY"
fi
# 새 이미지 pull
log "이미지 pull 중: $IMAGE"
docker pull "$IMAGE"
# 혹시 남아있는 동일 이름 컨테이너 정리
if docker ps -a --format '{{.Names}}' | grep -qx "$NEW_CONTAINER"; then
log "기존 $NEW_CONTAINER 컨테이너 정리"
docker stop "$NEW_CONTAINER" >/dev/null 2>&1 || true
docker rm "$NEW_CONTAINER" >/dev/null 2>&1 || true
fi
# 새 컨테이너 실행 (트래픽 전환 전까지 Docker 기본 bridge에만 둔다)
log "새 컨테이너($NEW_CONTAINER) 실행 중"
# Docker run 옵션 구성
DOCKER_OPTS=(
-d --name "$NEW_CONTAINER"
-e SPRING_PROFILES_ACTIVE=prod
-e DATABASE_HOST
-e DATABASE_USERNAME
-e DATABASE_PASSWORD
-e APP_JWT_SECRET
-e APP_JWT_ACCESS_TOKEN_EXPIRATION_SECONDS
-e APP_JWT_REFRESH_TOKEN_EXPIRATION_SECONDS
-e APP_OAUTH2_REDIRECT_SUCCESS_URL
-e APP_OAUTH2_EXTRA_INFO_URL
-e GOOGLE_OAUTH_CLIENT_ID
-e GOOGLE_OAUTH_CLIENT_SECRET
)
# Firebase + GCP 서비스 계정 설정 (선택적)
# 이 파일은 Firebase와 Vertex AI(Gemini) 인증에 모두 사용됨
if [ -f "/home/ubuntu/app/secrets/firebase-service-account.json" ]; then
DOCKER_OPTS+=(
-v /home/ubuntu/app/secrets/firebase-service-account.json:/app/secrets/firebase-service-account.json:ro
-e FIREBASE_SERVICE_ACCOUNT_PATH=/app/secrets/firebase-service-account.json
-e GOOGLE_APPLICATION_CREDENTIALS=/app/secrets/firebase-service-account.json
)
log "Firebase + GCP 서비스 계정 마운트됨"
fi
# S3 설정 (선택적)
if [ -n "${AWS_S3_BUCKET:-}" ]; then
DOCKER_OPTS+=(
-e AWS_S3_BUCKET
-e AWS_S3_REGION
-e AWS_S3_ACCESS_KEY
-e AWS_S3_SECRET_KEY
)
log "S3 설정 추가됨"
fi
# Gemini 설정 (선택적)
if [ -n "${GEMINI_PROJECT_ID:-}" ]; then
DOCKER_OPTS+=(
-e GEMINI_PROJECT_ID
-e GEMINI_ENABLED
-e GEMINI_MODEL
-e GEMINI_LOCATION
)
log "Gemini 설정 추가됨"
fi
docker run "${DOCKER_OPTS[@]}" "$IMAGE" >/dev/null
# 헬스체크 (docker inspect로 healthy 상태 확인)
# Docker health status가 start-period 직후 일시적으로 unhealthy가 될 수 있어
# unhealthy 즉시 실패 대신 전체 타임아웃까지 여유 있게 대기한다.
log "헬스체크 시작 (최대 ${HEALTH_TIMEOUT_SECONDS}초)"
HEALTH_PASSED=false
for i in $(seq 1 "$HEALTH_TIMEOUT_SECONDS"); do
RUNNING=$(docker inspect --format='{{.State.Running}}' "$NEW_CONTAINER" 2>/dev/null || echo "false")
STATUS=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}unknown{{end}}' "$NEW_CONTAINER" 2>/dev/null || echo "missing")
if [ "$RUNNING" != "true" ]; then
echo ">>> 새 컨테이너가 실행 중이 아닙니다. 롤백합니다." >&2
docker logs --tail=100 "$NEW_CONTAINER" >&2 || true
cleanup_new_container
exit 1
fi
if [ "$STATUS" = "healthy" ]; then
log "헬스체크 통과! (${i}초)"
HEALTH_PASSED=true
break
fi
if [ $((i % HEALTH_LOG_INTERVAL_SECONDS)) -eq 0 ]; then
log "헬스체크 대기 중... status=$STATUS (${i}/${HEALTH_TIMEOUT_SECONDS}초)"
fi
sleep 1
done
if [ "$HEALTH_PASSED" != "true" ]; then
echo ">>> 헬스체크 타임아웃. 롤백합니다." >&2
docker logs --tail=100 "$NEW_CONTAINER" >&2 || true
cleanup_new_container
exit 1
fi
# 새 컨테이너를 네트워크에 연결 (app alias 부여)
log "트래픽 전환 중"
docker network connect --alias app "$NETWORK" "$NEW_CONTAINER"
# 이전 컨테이너 graceful shutdown 후 정리
# stop 먼저: SIGTERM → readiness 503 → Caddy가 health check로 감지 후 pool에서 제거
# disconnect는 그 이후: Caddy가 감지한 뒤 네트워크 분리
if [ -n "$CURRENT" ] && docker ps -a --format '{{.Names}}' | grep -qx "$CURRENT"; then
log "이전 컨테이너($CURRENT) graceful shutdown 중"
docker stop "$CURRENT" >/dev/null 2>&1 || true
docker network disconnect "$NETWORK" "$CURRENT" 2>/dev/null || true
docker rm "$CURRENT" >/dev/null 2>&1 || true
fi
# 오래된 Docker 이미지 정리
docker image prune -f >/dev/null
log "배포 완료! 활성 컨테이너: $NEW_CONTAINER"