-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcompose-rollout.sh
More file actions
executable file
·663 lines (546 loc) · 21.5 KB
/
compose-rollout.sh
File metadata and controls
executable file
·663 lines (546 loc) · 21.5 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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
#!/usr/bin/env bash
#
# compose-rollout.sh — docker compose 기반 2인스턴스 롤링 배포
#
# 사용법:
# compose-rollout.sh <compose_file> <service> <db_reset:true|false> <db_volume>
# compose-rollout.sh <compose_file> reset-upstream <service>
#
# rolling deploy 동작:
# 1. 두 replica의 health 상태를 확인하여 unhealthy/missing을 먼저 배포
# 2. 각 replica를 drain → restart → rejoin 순서로 배포
# 3. unhealthy 컨테이너가 있어도 별도 옵션 없이 안전하게 배포
#
# 예시:
# compose-rollout.sh deploy/compose/tencent/dev/compose.yml app false deck-dev_db-data
# compose-rollout.sh deploy/compose/tencent/stage/compose.yml linkpie true deck-stage_db-data
# compose-rollout.sh deploy/compose/tencent/dev/compose.yml reset-upstream app
SCRIPT_VERSION="26.04.16"
[[ -n "${ZSH_VERSION:-}" ]] && emulate bash
set -o nounset -o errexit -o errtrace -o pipefail
SCRIPT_DIR=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
readonly BOLD_CYAN="\e[1;36m"
readonly BOLD_YELLOW="\e[1;33m"
readonly BOLD_RED="\e[1;91m"
readonly RESET="\e[0m"
readonly CONTAINER_STATE_STATUS_FORMAT='{{.State.Status}}'
readonly CONTAINER_HEALTH_STATUS_FORMAT='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'
: "${READINESS_TIMEOUT_SECONDS:=180}"
: "${READINESS_POLL_INTERVAL_SECONDS:=5}"
: "${READINESS_LOG_TAIL_LINES:=200}"
: "${IMAGE_KEEP_COUNT:=10}"
: "${NGINX_READY_TIMEOUT_SECONDS:=30}"
: "${NGINX_READY_POLL_INTERVAL_SECONDS:=2}"
: "${NGINX_READY_STABLE_SUCCESSES:=3}"
: "${NGINX_EXEC_RETRY_COUNT:=3}"
: "${FINAL_HEALTH_GATE_TIMEOUT_SECONDS:=30}"
: "${FINAL_HEALTH_GATE_POLL_INTERVAL_SECONDS:=5}"
# nginx upstream server 옵션: 3회 연속 실패 시 10초간 제외
readonly UPSTREAM_SERVER_OPTS="max_fails=3 fail_timeout=10s"
function log::error() {
printf "%bERROR: %s%b\n" "$BOLD_RED" "$*" "$RESET" 1>&2
}
function log::warning() {
printf "%bWARNING: %s%b\n" "$BOLD_YELLOW" "$*" "$RESET" 1>&2
}
function log::do() {
local line_no
line_no=$(caller | awk '{print $1}')
# shellcheck disable=SC2064
trap "log::error 'Failed at line $line_no: $*'" ERR
printf "%b+ %s%b\n" "$BOLD_CYAN" "$*" "$RESET" 1>&2
"$@"
}
function print_usage_and_exit() {
local code=${1:-0} out=2
[[ $code -eq 0 ]] && out=1
cat >&"${out}" <<END_OF_USAGE
Usage: compose-rollout.sh <compose_file> <service> <db_reset:true|false> <db_volume>
compose-rollout.sh <compose_file> reset-upstream <service>
ARGUMENTS:
compose_file compose.yml 파일 경로 (예: deploy/compose/tencent/dev/compose.yml)
service app | linkpie | deskpie
db_reset true | false
db_volume docker volume name for db reset
COMMANDS:
reset-upstream 배포 실패 후 down 상태가 남은 upstream을 모두 active로 복구
ENVIRONMENT VARIABLES:
READINESS_TIMEOUT_SECONDS default: 180
READINESS_POLL_INTERVAL_SECONDS default: 5
READINESS_LOG_TAIL_LINES default: 200
FINAL_HEALTH_GATE_TIMEOUT_SECONDS default: 30
FINAL_HEALTH_GATE_POLL_INTERVAL_SECONDS default: 5
IMAGE_KEEP_COUNT default: 10 (0 = skip cleanup)
END_OF_USAGE
exit "$code"
}
function rollout::dump_service_diagnostics() {
local service="$1"
local container_id="$2"
local tail_lines="$READINESS_LOG_TAIL_LINES"
echo >&2 ""
echo >&2 "## Diagnostics: ${service}"
log::do docker compose ps "$service" || true
log::do docker inspect "$container_id" || true
log::do docker compose logs --tail "$tail_lines" "$service" || true
}
function rollout::wait_for_service_ready() {
local service="$1"
local timeout_seconds="$READINESS_TIMEOUT_SECONDS"
local poll_interval_seconds="$READINESS_POLL_INTERVAL_SECONDS"
local attempts=$((timeout_seconds / poll_interval_seconds))
local attempt=1
local container_id
local state health
if [[ "$attempts" -lt 1 ]]; then
attempts=1
fi
container_id="$(log::do docker compose ps -q "$service")"
while [[ "$attempt" -le "$attempts" ]]; do
state="$(log::do docker inspect --format "$CONTAINER_STATE_STATUS_FORMAT" "$container_id")"
health="$(log::do docker inspect --format "$CONTAINER_HEALTH_STATUS_FORMAT" "$container_id")"
echo >&2 "# [${attempt}/${attempts}] ${service}: state=${state} health=${health}"
if [[ "$health" == "healthy" ]]; then
echo >&2 "# ${service} is ready"
return 0
fi
if [[ "$health" == "unhealthy" || "$state" == "exited" || "$state" == "dead" ]]; then
log::error "Readiness failed for ${service}: state=${state} health=${health}"
rollout::dump_service_diagnostics "$service" "$container_id"
return 1
fi
log::do sleep "$poll_interval_seconds"
attempt=$((attempt + 1))
done
log::error "Timed out waiting for ${service} readiness after ${timeout_seconds}s"
rollout::dump_service_diagnostics "$service" "$container_id"
return 1
}
# nginx upstream에서 replica를 제거 (down 처리)
function rollout::drain() {
local service="$1" # e.g. app
local replica="$2" # e.g. app-1
local upstream_file="/etc/nginx/upstreams/${service}/${replica}.conf"
rollout::ensure_nginx_available "$service"
echo >&2 ""
echo >&2 "## Drain ${replica} from nginx-${service}"
rollout::exec_nginx "$service" sh -c "echo 'server ${replica}:8011 down ${UPSTREAM_SERVER_OPTS};' > '${upstream_file}'"
rollout::exec_nginx "$service" nginx -t
rollout::exec_nginx "$service" nginx -s reload
echo >&2 "# ${replica} drained"
}
# nginx upstream에 replica를 복귀
function rollout::rejoin() {
local service="$1" # e.g. app
local replica="$2" # e.g. app-1
local upstream_file="/etc/nginx/upstreams/${service}/${replica}.conf"
rollout::ensure_nginx_available "$service"
echo >&2 ""
echo >&2 "## Rejoin ${replica} to nginx-${service}"
rollout::exec_nginx "$service" sh -c "echo 'server ${replica}:8011 ${UPSTREAM_SERVER_OPTS};' > '${upstream_file}'"
rollout::exec_nginx "$service" nginx -t
rollout::exec_nginx "$service" nginx -s reload
echo >&2 "# ${replica} rejoined"
}
function rollout::restart_and_wait() {
local service="$1"
echo >&2 ""
echo >&2 "## Restart ${service}"
rollout::remove_stale_containers "$service"
log::do docker compose up -d --no-deps "$service"
log::do docker compose ps "$service"
rollout::wait_for_service_ready "$service"
}
# 이전 배포 실패로 남은 <hash>_<project>-<service> 고아 컨테이너를 정리한 뒤
# compose-managed 컨테이너를 stop + rm 하여 docker compose up이 recreate 대신
# clean create 하도록 한다.
function rollout::remove_stale_containers() {
local service="$1"
local project_name
project_name="$(docker compose config --format json | jq -r '.name')"
local stale
stale="$(docker ps -a --format '{{.Names}}' | grep -E "^[0-9a-f]+_${project_name}-${service}-" || true)"
if [[ -n "$stale" ]]; then
echo >&2 "# Removing stale containers: ${stale}"
xargs docker rm -f <<< "$stale"
fi
log::do docker compose rm -fs "$service"
}
# INT/TERM 수신 시 컨테이너 health를 확인하고, healthy면 rejoin, 아니면 down 유지
function rollout::rejoin_if_healthy() {
local service="$1"
local replica="$2"
local container_id health
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || return 0
[[ -z "$container_id" ]] && return 0
health="$(docker inspect --format "$CONTAINER_HEALTH_STATUS_FORMAT" "$container_id" 2>/dev/null)" || return 0
if [[ "$health" == "healthy" ]]; then
rollout::rejoin "$service" "$replica"
else
log::warning "${replica} is ${health} — leaving down in upstream (manual rejoin required)"
fi
}
function rollout::deploy_replica() {
local service="$1" # e.g. app
local replica="$2" # e.g. app-1
local container_id ps_exit=0
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || ps_exit=$?
if [[ "$ps_exit" -ne 0 ]]; then
log::error "docker compose ps -q ${replica} failed (exit ${ps_exit})"
return 1
fi
if [[ -n "$container_id" ]]; then
rollout::drain "$service" "$replica"
else
echo >&2 ""
echo >&2 "## Skip drain: ${replica} not running"
fi
# restart 실패 시: errexit로 즉시 종료 — unhealthy 인스턴스를 upstream에 올리지 않음
# INT/TERM 시: health 확인 후 healthy면 rejoin, 아니면 down 유지
# SC2064: service/replica는 trap 설정 시점에 즉시 확장되어야 하므로 double quotes 사용
# shellcheck disable=SC2064
trap "rollout::rejoin_if_healthy '${service}' '${replica}'; trap - INT TERM" INT TERM
rollout::restart_and_wait "$replica"
trap - INT TERM
rollout::rejoin "$service" "$replica"
}
# 두 replica의 health를 확인하고, unhealthy/missing을 먼저 배포하도록 순서를 결정한다.
# 출력: replica 이름을 줄바꿈으로 구분 (unhealthy 먼저, healthy 나중)
function rollout::resolve_deploy_order() {
local service="$1"
local -a unhealthy=() healthy=()
local -a deploy_order=()
echo >&2 ""
echo >&2 "## Health Check: ${service}"
for i in 1 2; do
local replica="${service}-${i}"
local container_id health
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || true
if [[ -z "$container_id" ]]; then
echo >&2 "# ${replica}: missing → deploy first"
unhealthy+=("$replica")
continue
fi
health="$(docker inspect --format "$CONTAINER_HEALTH_STATUS_FORMAT" "$container_id" 2>/dev/null)" || true
if [[ "$health" == "healthy" ]]; then
echo >&2 "# ${replica}: healthy"
healthy+=("$replica")
else
echo >&2 "# ${replica}: ${health} → deploy first"
unhealthy+=("$replica")
fi
done
if [[ "${#unhealthy[@]}" -gt 0 ]]; then
deploy_order+=("${unhealthy[@]}")
fi
if [[ "${#healthy[@]}" -gt 0 ]]; then
deploy_order+=("${healthy[@]}")
fi
printf '%s\n' "${deploy_order[@]}"
}
function rollout::build_runtime_upstream_line() {
local replica="$1"
local container_id health
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || true
if [[ -z "$container_id" ]]; then
printf 'server 127.0.0.1:1 down %s;\n' "$UPSTREAM_SERVER_OPTS"
return 0
fi
health="$(docker inspect --format "$CONTAINER_HEALTH_STATUS_FORMAT" "$container_id" 2>/dev/null)" || true
if [[ "$health" == "healthy" ]]; then
printf 'server %s:8011 %s;\n' "$replica" "$UPSTREAM_SERVER_OPTS"
else
printf 'server %s:8011 down %s;\n' "$replica" "$UPSTREAM_SERVER_OPTS"
fi
}
function rollout::rewrite_upstream_files_from_runtime() {
local service="$1"
local upstream_dir="./nginx/upstreams/${service}"
mkdir -p "$upstream_dir"
for i in 1 2; do
local replica="${service}-${i}"
local upstream_file="${upstream_dir}/${replica}.conf"
local upstream_line
upstream_line="$(rollout::build_runtime_upstream_line "$replica")"
log::do rm -f "$upstream_file"
printf '%s' "$upstream_line" > "$upstream_file"
done
}
function rollout::start_nginx_and_wait() {
local nginx="$1"
log::do docker compose up -d --no-deps "$nginx"
log::do docker compose ps "$nginx"
rollout::wait_for_nginx_exec_ready "$nginx"
}
function rollout::wait_for_nginx_exec_ready() {
local nginx="$1"
local timeout_seconds="$NGINX_READY_TIMEOUT_SECONDS"
local poll_interval_seconds="$NGINX_READY_POLL_INTERVAL_SECONDS"
local stable_successes_required="$NGINX_READY_STABLE_SUCCESSES"
local attempts=$((timeout_seconds / poll_interval_seconds))
local attempt=1
local container_id state
local stable_successes=0
if [[ "$attempts" -lt 1 ]]; then
attempts=1
fi
container_id="$(docker compose ps -q "$nginx" 2>/dev/null)" || true
if [[ -z "$container_id" ]]; then
log::error "${nginx} container id not found"
return 1
fi
while [[ "$attempt" -le "$attempts" ]]; do
state="$(docker inspect --format "$CONTAINER_STATE_STATUS_FORMAT" "$container_id" 2>/dev/null)" || true
echo >&2 "# [${attempt}/${attempts}] ${nginx}: state=${state:-unknown}"
if [[ "$state" == "running" ]] && docker compose exec "$nginx" true >/dev/null 2>&1; then
stable_successes=$((stable_successes + 1))
echo >&2 "# ${nginx}: exec-ready ${stable_successes}/${stable_successes_required}"
if [[ "$stable_successes" -ge "$stable_successes_required" ]]; then
echo >&2 "# ${nginx} is ready for exec"
return 0
fi
else
stable_successes=0
fi
if [[ "$state" == "exited" || "$state" == "dead" ]]; then
log::error "${nginx} failed to start: state=${state}"
rollout::dump_service_diagnostics "$nginx" "$container_id"
return 1
fi
log::do sleep "$poll_interval_seconds"
attempt=$((attempt + 1))
done
log::error "Timed out waiting for ${nginx} to accept exec after ${timeout_seconds}s"
rollout::dump_service_diagnostics "$nginx" "$container_id"
return 1
}
# 이전 실패/재기동으로 nginx가 crash loop에 빠져 있으면 현재 replica 상태를 기준으로 복구한다.
function rollout::ensure_nginx_available() {
local service="$1"
local nginx="nginx-${service}"
local container_id state
container_id="$(docker compose ps -q "$nginx" 2>/dev/null)" || true
if [[ -n "$container_id" ]]; then
state="$(docker inspect --format "$CONTAINER_STATE_STATUS_FORMAT" "$container_id" 2>/dev/null)" || true
if [[ "$state" == "running" ]]; then
if docker compose exec "$nginx" true >/dev/null 2>&1; then
echo >&2 "# ${nginx} is ready for exec"
return 0
fi
log::warning "${nginx} is running but exec is unavailable — rebuilding upstream files and restarting"
else
log::warning "${nginx} is ${state:-unknown} — rebuilding upstream files and restarting"
fi
else
log::warning "${nginx} is missing — rebuilding upstream files and starting"
fi
rollout::rewrite_upstream_files_from_runtime "$service"
rollout::start_nginx_and_wait "$nginx"
}
function rollout::restart_nginx_with_current_upstreams() {
local service="$1"
local nginx="nginx-${service}"
log::warning "${nginx} exec failed — restarting with current upstream files"
rollout::start_nginx_and_wait "$nginx"
}
function rollout::exec_nginx() {
local service="$1"
shift
local nginx="nginx-${service}"
local max_attempts="$NGINX_EXEC_RETRY_COUNT"
local attempt=1
if [[ "$max_attempts" -lt 1 ]]; then
max_attempts=1
fi
while [[ "$attempt" -le "$max_attempts" ]]; do
if log::do docker compose exec "$nginx" "$@"; then
return 0
fi
if [[ "$attempt" -ge "$max_attempts" ]]; then
return 1
fi
rollout::restart_nginx_with_current_upstreams "$service"
attempt=$((attempt + 1))
done
}
function rollout::final_health_gate() {
local service="$1"
local timeout_seconds="$FINAL_HEALTH_GATE_TIMEOUT_SECONDS"
local poll_interval_seconds="$FINAL_HEALTH_GATE_POLL_INTERVAL_SECONDS"
local attempts=$((timeout_seconds / poll_interval_seconds))
local attempt=1
if [[ "$attempts" -lt 1 ]]; then
attempts=1
fi
echo >&2 ""
echo >&2 "## Final Health Gate: ${service} (timeout=${timeout_seconds}s)"
while [[ "$attempt" -le "$attempts" ]]; do
local all_healthy=true
for i in 1 2; do
local replica="${service}-${i}"
local container_id health
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || true
if [[ -z "$container_id" ]]; then
all_healthy=false
break
fi
health="$(docker inspect --format "$CONTAINER_HEALTH_STATUS_FORMAT" "$container_id" 2>/dev/null)" || true
if [[ "$health" != "healthy" ]]; then
all_healthy=false
echo >&2 "# [${attempt}/${attempts}] ${replica}: health=${health}"
break
fi
done
if [[ "$all_healthy" == "true" ]]; then
echo >&2 "# ${service}-1 and ${service}-2 are both healthy"
return 0
fi
log::do sleep "$poll_interval_seconds"
attempt=$((attempt + 1))
done
log::error "Final health gate failed: not all replicas healthy within ${timeout_seconds}s"
for i in 1 2; do
local replica="${service}-${i}"
local container_id
container_id="$(docker compose ps -q "$replica" 2>/dev/null)" || true
[[ -n "$container_id" ]] && rollout::dump_service_diagnostics "$replica" "$container_id"
done
return 1
}
function rollout::rolling_deploy() {
local service="$1"
local -a deploy_order=()
echo >&2 ""
echo >&2 "## Rolling Deploy: ${service}"
while IFS= read -r replica; do
[[ -n "$replica" ]] && deploy_order+=("$replica")
done < <(rollout::resolve_deploy_order "$service")
for replica in "${deploy_order[@]}"; do
rollout::deploy_replica "$service" "$replica"
done
rollout::final_health_gate "$service"
log::do docker compose ps "${service}-1" "${service}-2"
}
# 배포 실패로 down 상태가 남은 upstream을 모두 active로 복구
function rollout::reset_upstream() {
local service="$1"
echo >&2 ""
echo >&2 "## Reset Upstream: ${service}"
rollout::ensure_nginx_available "$service"
for i in 1 2; do
local replica="${service}-${i}"
local upstream_file="/etc/nginx/upstreams/${service}/${replica}.conf"
rollout::exec_nginx "$service" sh -c "echo 'server ${replica}:8011 ${UPSTREAM_SERVER_OPTS};' > '${upstream_file}'"
done
rollout::exec_nginx "$service" nginx -t
rollout::exec_nginx "$service" nginx -s reload
echo >&2 "# All replicas active for ${service}"
}
function rollout::db_reset() {
local service="$1"
local db_volume="$2"
echo >&2 ""
echo >&2 "## DB Reset: ${service}"
log::do docker compose rm -fs "${service}-1" "${service}-2" "nginx-${service}" db
log::do docker volume rm -f "$db_volume"
log::do docker compose up -d db
rollout::wait_for_service_ready db
# nginx upstream 설정을 placeholder로 초기화하여 backend 컨테이너 없이 시작 가능하게 한다.
# nginx는 startup 시 upstream hostname을 DNS 해석하므로, 컨테이너가 없으면 시작 실패한다.
# 127.0.0.1 + down으로 DNS 해석 없이 유효한 설정을 제공한다.
# upstream 설정 파일은 nginx 컨테이너(root)가 생성하여 deploy user가 직접 덮어쓸 수 없다.
# 디렉토리는 deploy user 소유이므로 rm → 재생성으로 우회한다.
local upstream_dir="./nginx/upstreams/${service}"
mkdir -p "$upstream_dir"
for i in 1 2; do
log::do rm -f "${upstream_dir}/${service}-${i}.conf"
printf 'server 127.0.0.1:1 down %s;\n' "$UPSTREAM_SERVER_OPTS" > "${upstream_dir}/${service}-${i}.conf"
done
echo >&2 "# Upstream configs reset to placeholder (all down)"
log::do docker compose up -d --no-deps "nginx-${service}"
}
# 배포 완료 후 사용하지 않는 이미지를 정리한다.
# 1. dangling(태그 없는) 이미지 전량 삭제
# 2. 서비스 이미지를 생성일 역순으로 정렬하여 최근 IMAGE_KEEP_COUNT개만 유지
function cleanup::prune_images() {
local image_repo="$1"
local keep_count="$IMAGE_KEEP_COUNT"
if [[ "$keep_count" -eq 0 ]]; then
echo >&2 "# IMAGE_KEEP_COUNT=0 — skipping image cleanup"
return 0
fi
echo >&2 ""
echo >&2 "## Image Cleanup"
# dangling 이미지 삭제
local dangling_count
dangling_count="$(docker images -f 'dangling=true' -q | wc -l)"
if [[ "$dangling_count" -gt 0 ]]; then
echo >&2 "# Removing ${dangling_count} dangling images"
log::do docker image prune -f
fi
# 서비스 이미지 중 오래된 것 삭제 (최근 keep_count개 유지)
local -a old_images=()
while IFS= read -r image_id; do
[[ -n "$image_id" ]] && old_images+=("$image_id")
done < <(docker images "$image_repo" --format '{{.ID}}' | tail -n +"$((keep_count + 1))")
if [[ ${#old_images[@]} -gt 0 ]]; then
echo >&2 "# Removing ${#old_images[@]} old images for ${image_repo} (keeping ${keep_count})"
log::do docker rmi "${old_images[@]}" || true
else
echo >&2 "# No old images to remove for ${image_repo}"
fi
echo >&2 "# Cleanup complete"
}
function main::validate_service() {
case "$1" in
app|linkpie|deskpie) ;;
*) log::error "service must be app, linkpie, or deskpie"; print_usage_and_exit 1 ;;
esac
}
function main::enter_compose_dir() {
local compose_file="$1"
if [[ ! -f "$compose_file" ]]; then
log::error "compose file not found: ${compose_file}"
return 1
fi
cd "$(dirname "$compose_file")"
}
function main() {
local compose_file="${1:-}"
[[ -n "$compose_file" ]] || print_usage_and_exit 1
if [[ "${2:-}" == "reset-upstream" ]]; then
local service="${3:-}"
[[ -n "$service" ]] || print_usage_and_exit 1
main::validate_service "$service"
main::enter_compose_dir "$compose_file"
echo >&2 "### Compose Rollout ${SCRIPT_VERSION} ###"
echo >&2 "# script: ${SCRIPT_DIR}/compose-rollout.sh"
echo >&2 "# compose_file=${compose_file} command=reset-upstream service=${service}"
rollout::reset_upstream "$service"
return 0
fi
local service="${2:-}"
local db_reset="${3:-}"
local db_volume="${4:-}"
[[ -n "$service" && -n "$db_reset" && -n "$db_volume" ]] || print_usage_and_exit 1
main::validate_service "$service"
main::enter_compose_dir "$compose_file"
case "$db_reset" in
true|false) ;;
*) log::error "db_reset must be true or false"; print_usage_and_exit 1 ;;
esac
echo >&2 "### Compose Rollout ${SCRIPT_VERSION} ###"
echo >&2 "# script: ${SCRIPT_DIR}/compose-rollout.sh"
echo >&2 "# compose_file=${compose_file} service=${service} db_reset=${db_reset} db_volume=${db_volume}"
log::do docker compose pull "${service}-1" "${service}-2"
if [[ "$db_reset" == "true" ]]; then
rollout::db_reset "$service" "$db_volume"
fi
rollout::rolling_deploy "$service"
local image_repo
image_repo="$(log::do docker compose images "${service}-1" --format json | jq -r '.[0].Repository')"
cleanup::prune_images "$image_repo"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi