#!/usr/bin/env bash ############################################################################### # [폐쇄망 안에서 실행] compose 없이 docker/podman run 만으로 전체 스택을 띄운다. # # ./bin/up-run.sh # docker run (= bin/up-docker-run.sh) # ./bin/up-run.sh podman # podman run (= bin/up-podman-run.sh) # ./bin/up-run.sh --dry-run # 아무것도 만들지 않고 실행될 명령만 출력 # # 토폴로지를 여기에 다시 적지 않는다. compose.yml 을 읽어서 그대로 run 인자로 # 옮긴 뒤(bin/_compose_to_plan.py) 순서대로 실행한다. compose 파일이 정본이고, # compose 바이너리가 없는 호스트에서도 같은 스택이 뜬다. # # 되돌리기: ./bin/down.sh run [--volumes] ############################################################################### . "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh" parse_common_args "$@" resolve_engine "${ENGINE_ARG:-docker}" load_env check_replica_counts check_litellm_config require_conf [ "${DRY_RUN}" = true ] || prepare_log_dirs COMPOSE_FILE="${DEPLOY_DIR}/compose.yml" # bin/apply-patches.sh 가 만든 override 가 있으면 함께 읽는다 (현장 패치). PATCH_OVERLAY=() if [ -f "${DEPLOY_DIR}/compose.patches.yml" ]; then PATCH_OVERLAY=(--overlay "${DEPLOY_DIR}/compose.patches.yml") log "현장 패치 적용: compose.patches.yml" fi # .env 의 EXPOSE_*_PORT 로 호스트 포트 override 를 만든다 (없으면 파일도 지운다). write_ports_override [ -n "${PORTS_OVERRIDE}" ] && PATCH_OVERLAY+=(--overlay "${PORTS_OVERRIDE}") # podman 의 DNS 주소는 네트워크마다 달라 nginx 에 127.0.0.11 을 못 박을 수 없다. if [ "${ENGINE_KIND}" = podman ]; then export NGINX_UPSTREAM="${NGINX_UPSTREAM:-podman}" else export NGINX_UPSTREAM="${NGINX_UPSTREAM:-docker}" fi # ── compose 파일 → 실행 계획 ──────────────────────────────────────────────── # python3 는 대부분의 리눅스 서버에 있다. PyYAML 이 없으면 번들에 든 최소 파서를 쓴다. command -v python3 >/dev/null 2>&1 \ || die "python3 가 없다. compose 파일을 해석할 수 없으므로 compose 로 띄운다: ./bin/up-compose.sh" PLAN="$(python3 "${DEPLOY_DIR}/bin/_compose_to_plan.py" "${COMPOSE_FILE}" "${DEPLOY_DIR}" --engine "${ENGINE_KIND}" ${PATCH_OVERLAY[@]+"${PATCH_OVERLAY[@]}"})" \ || die "compose 파일을 해석하지 못했다. compose 가 있으면 ./bin/up-compose.sh 를 쓴다." plan_get() { printf '%s' "${PLAN}" | python3 -c 'import json,sys; print(json.load(sys.stdin)'"$1"')'; } # 계획의 문자열 배열을 NUL 로 흘려 보낸다 (공백·따옴표가 든 인자를 안전하게 옮긴다). plan_argv() { printf '%s' "${PLAN}" | python3 -c ' import json, sys plan = json.load(sys.stdin) for a in eval("plan" + sys.argv[1]): sys.stdout.write(str(a) + "\0") ' "$1"; } PROJECT="$(plan_get '["project"]')" PARSER="$(plan_get '["parser"]')" log "compose 해석: ${COMPOSE_FILE} (파서: ${PARSER}, 프로젝트: ${PROJECT})" log "이미지 적재 확인" while IFS= read -r -d '' tag; do engine image inspect "${tag}" >/dev/null 2>&1 \ || die "이미지 ${tag} 가 없다. 먼저 ./bin/load-images.sh ${ENGINE_KIND} 를 실행한다." done < <(plan_argv '["services"][s]["image"] for s in plan["order"]' 2>/dev/null || printf '') # ── 실행할 명령만 보기 ────────────────────────────────────────────────────── # --dry-run 은 아무것도 만들지 않는다. 네트워크·볼륨·로그 디렉터리 생성보다 먼저 처리한다. print_plan() { printf '%s' "${PLAN}" | python3 -c ' import json, shlex, sys plan = json.load(sys.stdin) engine = sys.argv[1] for n in plan["networks"]: print("%s network create %s%s" % (engine, "--internal " if n["internal"] else "", n["name"])) for v in plan["volumes"]: print("%s volume create %s" % (engine, v)) for name in plan["order"]: s = plan["services"][name] for replica in range(1, s["replicas"] + 1): container = s["name"] if replica == 1 else "%s-%d" % (s["name"], replica) argv = [engine, "run", "-d", "--name", container, "--network", s["networks"][0], "--network-alias", s["alias"]] argv += s["opts"] + [s["image"]] + s["command"] print(" ".join(shlex.quote(a) for a in argv)) for extra in s["networks"][1:]: print("%s network connect --alias %s %s %s" % (engine, s["alias"], extra, container)) ' "${ENGINE}" } if [ "${DRY_RUN}" = true ]; then log "dry-run — 아래 명령을 실행하지 않고 출력만 한다" print_plan exit 0 fi # ── 네트워크 · 볼륨 ───────────────────────────────────────────────────────── while IFS=$'\t' read -r net internal; do [ -n "${net}" ] || continue if engine network inspect "${net}" >/dev/null 2>&1; then log "네트워크 있음: ${net}" else log "네트워크 생성: ${net}${internal:+ (internal)}" # shellcheck disable=SC2086 engine network create ${internal:+--internal} "${net}" >/dev/null fi done < <(printf '%s' "${PLAN}" | python3 -c ' import json, sys for n in json.load(sys.stdin)["networks"]: print("%s\t%s" % (n["name"], "1" if n["internal"] else "")) ') while IFS= read -r vol; do [ -n "${vol}" ] || continue engine volume inspect "${vol}" >/dev/null 2>&1 || { log "볼륨 생성: ${vol}"; engine volume create "${vol}" >/dev/null; } done < <(plan_get '["volumes"]' | tr -d "[]'\" " | tr ',' '\n') # ── 서비스 기동 ───────────────────────────────────────────────────────────── read_argv() { # 계획에서 배열 하나를 읽어 전역 ARGV 에 담는다 ARGV=() while IFS= read -r -d '' item; do ARGV+=("${item}"); done < <(plan_argv "$1") } svc_field() { printf '%s' "${PLAN}" | python3 -c 'import json,sys; print(json.load(sys.stdin)["services"][sys.argv[1]][sys.argv[2]])' "$1" "$2"; } ORDER=() while IFS= read -r s; do [ -n "${s}" ] && ORDER+=("${s}"); done < <(printf '%s' "${PLAN}" | python3 -c 'import json,sys; print("\n".join(json.load(sys.stdin)["order"]))') step=0 for svc in "${ORDER[@]}"; do step=$((step + 1)) name="$(svc_field "${svc}" name)" replicas="$(svc_field "${svc}" replicas)" image="$(svc_field "${svc}" image)" alias="$(svc_field "${svc}" alias)" one_shot="$(svc_field "${svc}" one_shot)" has_hc="$(svc_field "${svc}" has_healthcheck)" NETS=() while IFS= read -r n; do [ -n "${n}" ] && NETS+=("${n}"); done \ < <(printf '%s' "${PLAN}" | python3 -c 'import json,sys; print("\n".join(json.load(sys.stdin)["services"][sys.argv[1]]["networks"]))' "${svc}") read_argv '["services"]["'"${svc}"'"]["opts"]'; OPTS=("${ARGV[@]}") read_argv '["services"]["'"${svc}"'"]["command"]'; CMD=("${ARGV[@]}") # 이전 기동보다 수를 줄였으면 남은 복제 컨테이너도 제거한다. while IFS= read -r existing; do case "${existing}" in "${name}"-*) suffix="${existing#"${name}"-}" case "${suffix}" in ''|*[!0-9]*) continue ;; esac if [ "${suffix}" -gt "${replicas}" ]; then log "초과 복제 제거: ${existing}" engine rm -f "${existing}" >/dev/null fi ;; esac done < <(engine ps -a --format '{{.Names}}') for ((replica=1; replica<=replicas; replica++)); do container="${name}" [ "${replica}" -eq 1 ] || container="${name}-${replica}" engine container inspect "${container}" >/dev/null 2>&1 && { log "재생성: ${container}"; engine rm -f "${container}" >/dev/null; } log "${step}/${#ORDER[@]} 기동: ${container} (${image})" engine run -d --name "${container}" \ --network "${NETS[0]}" --network-alias "${alias}" \ "${OPTS[@]}" "${image}" ${CMD[@]+"${CMD[@]}"} >/dev/null # compose 가 서비스에 네트워크를 여러 개 준 경우 — run 은 --network 를 하나만 받는다. for extra in "${NETS[@]:1}"; do engine network connect --alias "${alias}" "${extra}" "${container}" >/dev/null 2>&1 || true done # compose 의 depends_on condition 을 여기서 대신 지킨다. if [ "${one_shot}" = True ]; then wait_exit0 "${container}" 1800 elif [ "${has_hc}" = True ]; then wait_healthy "${container}" 600 fi done done log "기동 완료" engine ps --filter "name=${PROJECT}-" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' cat <