1. 배경 (Background)
당신은 GPU 서버 관리팀의 일원입니다. 최근 교내 정전 사태로 인해 서버 전체가 재부팅되는 사고가 있었습니다. 이 과정에서 Docker 컨테이너들이 재시작 되었으나, DB에 저장된 설정 정보와 실제 구동 중인 컨테이너 상태 간에 데이터 불일치(Port, UID/GID 등)가 발생했습니다.
우선 수동으로 복구를 완료했으나, 향후 동일한 사태를 대비해 동료 A와 함께 자동화 스크립트(sync_containers.sh)를 작성하는 업무를 맡았습니다. 동료 A는 빠르게 스크립트를 작성하고, 당신에게 전달해주었습니다. 당신의 임무는 이 스크립트를 다른 동료도 쉽게 사용할 수 있도록 기술 문서를 작성하여 배포하는 것입니다.
2. 목표 (Objectives)
동료 A가 작성한 쉘 스크립트 코드를 분석하고, 다른 팀원들도 이 도구를 이해하고 사용할 수 있도록 하기 위한 기술 문서를 노션으로 작성하십시오.
3. 필수 포함 항목
제출하는 PDF 문서에는 다음의 내용이 반드시 포함되어야 합니다. (목차 구성은 자유입니다.)
- 스크립트 개요: 이 스크립트가 해결하고자 하는 문제와 핵심 동작 로직(Flow) 설명
- 사용 가이드:
- 코드 분석:
4. 첨부 자료 (Source Code)
- 파일명: sync_containers.sh
- 작성자: 동료 A
#!/bin/bash
# ==========================================================
# sync_containers.sh
# DB 기준으로 컨테이너 상태 동기화
# 이미지/버전/UID/GID/포트 불일치 시 자동 recreate / 정지된 컨테이너 재시작
# – –dry-run: 실제 실행하지 않고 시뮬레이션만
# – –auto-delete: DB에 없는 컨테이너 자동 삭제
# ==========================================================
DB_ADDRESS=192.168.2.11
DB_PORT=3307
DB_NAME=”nfs_db”
DB_USER=”nfs_user”
DB_PASSWORD=”nfs_password”
DRY_RUN=false
AUTO_DELETE=false
# 옵션 파싱
for arg in “$@”; do
case “$arg” in
–dry-run) DRY_RUN=true ;;
–auto-delete) AUTO_DELETE=true ;;
esac
done
echo “[INFO] Using DB: $DB_NAME at $DB_ADDRESS:$DB_PORT”
echo “[INFO] Options: dry-run=$DRY_RUN, auto-delete=$AUTO_DELETE”
# MySQL 접속 설정 파일 생성
cat <<EOF > ~/.my.cnf
[client]
user=$DB_USER
password=$DB_PASSWORD
host=$DB_ADDRESS
port=$DB_PORT
EOF
chmod 600 ~/.my.cnf
# DB에서 컨테이너 목록 불러오기
containers=$(mysql -N -D $DB_NAME -e “
SELECT dc.container_name, dc.image, dc.image_version,
u.ubuntu_username, u.ubuntu_uid, u.ubuntu_gid,
dc.server_id, dc.id
FROM docker_container dc
JOIN user u
ON dc.user_id=u.id
WHERE dc.existing=1;
“)
echo “[INFO] Loaded $(echo “$containers” | wc -l) containers from DB.”
# 서버 컨테이너 상태 확인
docker ps -a –format “{{.Names}} {{.Status}}” > /tmp/docker_status.txt
while read -r cname image version uname uid gid sid dbid; do
server_status=$(grep -w “$cname” /tmp/docker_status.txt | awk ‘{print $2}’)
# (1) DB에 있고 서버에는 없는 경우
if [ -z “$server_status” ]; then
echo “[CREATE] $cname (image=$image:$version, user=$uname)”
ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number, purpose_of_use FROM used_ports
WHERE docker_container_record_id=$dbid;
“)
port_args=””
while read -r port purpose; do
[ -z “$port” ] && continue
if ss -tulpn | grep -q “:$port “; then
echo “[SKIP] Port $port ($purpose) already in use”
continue
fi
case “$purpose” in
ssh) port_args=”$port_args -p ${port}:22″ ;;
“jupyter notebook”) port_args=”$port_args -p ${port}:8888″ ;;
*) port_args=”$port_args -p ${port}:${port}” ;;
esac
done <<< “$ports”
if $DRY_RUN; then
echo “[DRY-RUN] docker run -dit $port_args –name $cname …”
else
docker run -dit \\
–name “$cname” \\
$port_args \\
-e USER_ID=$uname -e UID=$uid -e GID=$gid \\
dguailab/$image:$version
fi
continue
fi
# (2) 서버에는 있는데 정지된 경우
if [ “$server_status” == “Exited” ]; then
if $DRY_RUN; then
echo “[DRY-RUN] restart $cname”
else
echo “[RESTART] $cname”
docker start “$cname”
fi
continue
fi
# (3) 서버/DB 세부정보 비교 (이미지/UID/GID/포트)
mismatch=false
actual_image=$(docker inspect –format ‘{{.Config.Image}}’ “$cname” 2>/dev/null)
if [ “$actual_image” != “dguailab/$image:$version” ]; then
echo “[MISMATCH] Image differs: DB=$image:$version, Actual=$actual_image”
mismatch=true
fi
actual_uid=$(docker inspect –format ‘{{range .Config.Env}}{{println .}}{{end}}’ “$cname” | grep ‘^UID=’ | cut -d= -f2)
actual_gid=$(docker inspect –format ‘{{range .Config.Env}}{{println .}}{{end}}’ “$cname” | grep ‘^GID=’ | cut -d= -f2)
if [ “$actual_uid” != “$uid” ] || [ “$actual_gid” != “$gid” ]; then
echo “[MISMATCH] UID/GID differs: DB=$uid/$gid, Actual=$actual_uid/$actual_gid”
mismatch=true
fi
db_ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number FROM used_ports
WHERE docker_container_record_id=$dbid;
” | sort)
actual_ports_sorted=$(docker inspect “$cname” \\
| jq -r ‘[.[] | .NetworkSettings.Ports | to_entries[] | .value[]?.HostPort] | unique | .[]’ \\
| sort -n | tr ‘\\n’ ‘ ‘ | sed ‘s/ *$//’)
if [ “$db_ports” != “$actual_ports” ]; then
echo “[MISMATCH] Ports differ”
echo ” DB: $db_ports”
echo ” Actual: $actual_ports”
mismatch=true
fi
# 불일치 시 재생성
if $mismatch; then
if $DRY_RUN; then
echo “[DRY-RUN] Would recreate $cname”
else
echo “[RECREATE] $cname”
docker rm -f “$cname”
ports=$(mysql -N -D $DB_NAME -e “
SELECT port_number, purpose_of_use FROM used_ports
WHERE docker_container_record_id=$dbid;
“)
port_args=””
while read -r port purpose; do
[ -z “$port” ] && continue
case “$purpose” in
ssh) port_args=”$port_args -p ${port}:22″ ;;
“jupyter notebook”) port_args=”$port_args -p ${port}:8888″ ;;
*) port_args=”$port_args -p ${port}:${port}” ;;
esac
done <<< “$ports”
docker run -dit \\
–name “$cname” \\
$port_args \\
-e USER_ID=$uname -e UID=$uid -e GID=$gid \\
dguailab/$image:$version
fi
else
echo “[OK] $cname is running and matches DB”
fi
done <<<“$containers”
# (4) 서버에 있고 DB에는 없는 경우
server_only=$(comm -23 <(awk ‘{print $1}’ /tmp/docker_status.txt | sort) \\
<(echo “$containers” | awk ‘{print $1}’ | sort))
if [ -n “$server_only” ]; then
if $AUTO_DELETE; then
for cname in $server_only; do
if $DRY_RUN; then
echo “[DRY-RUN] Would delete $cname (not in DB)”
else
echo “[DELETE] $cname (not in DB)”
docker rm -f “$cname”
fi
done
else
echo “[WARN] Containers on server but not in DB:”
echo “$server_only”
fi
fi
echo “[DONE] Sync completed.”
Cf. 위 source 코드를 이해하기 위한 추가자료
— Create the database with explicit character set
CREATE DATABASE IF NOT EXISTS nfs_db CHARACTER
SET
= utf8mb4 COLLATE = utf8mb4_unicode_ci;
USE nfs_db;
— Create used_ids table for ID management
CREATE TABLE
used_ids (id INT PRIMARY KEY AUTO_INCREMENT) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create group table
CREATE TABLE
`group` (
id INT PRIMARY KEY AUTO_INCREMENT,
ubuntu_groupname VARCHAR(255) NOT NULL,
ubuntu_gid INT NOT NULL,
UNIQUE KEY unique_gid (ubuntu_gid),
FOREIGN KEY (ubuntu_gid) REFERENCES used_ids (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create user table without circular references
CREATE TABLE
user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
ubuntu_username VARCHAR(255) NOT NULL,
ubuntu_uid INT NOT NULL,
ubuntu_gid INT,
note TEXT,
UNIQUE KEY unique_uid (ubuntu_uid),
FOREIGN KEY (ubuntu_uid) REFERENCES used_ids (id),
FOREIGN KEY (ubuntu_gid) REFERENCES `group` (ubuntu_gid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create docker_container table
CREATE TABLE
docker_container (
id INT PRIMARY KEY AUTO_INCREMENT,
image VARCHAR(255) NOT NULL,
image_version VARCHAR(50) NOT NULL,
container_id VARCHAR(64) NOT NULL,
container_name VARCHAR(255) NOT NULL,
server_id VARCHAR(255) NOT NULL,
expiring_at DATETIME NOT NULL,
deleted_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
existing BOOLEAN DEFAULT TRUE,
created_by VARCHAR(255),
user_id INT,
UNIQUE KEY unique_container (container_id),
FOREIGN KEY (user_id) REFERENCES user (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Create used_ports table after docker_container exists
CREATE TABLE
used_ports (
port_number INT PRIMARY KEY,
docker_container_record_id INT,
purpose_of_use VARCHAR(255),
FOREIGN KEY (docker_container_record_id) REFERENCES docker_container (id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
— Add indexes
CREATE INDEX idx_container_existing ON docker_container (existing);
CREATE INDEX idx_container_expiring ON docker_container (expiring_at);
CREATE INDEX idx_user_username ON user (ubuntu_username);
— Verify character set settings
SET
NAMES utf8mb4;
CREATE VIEW
user_container_info AS
SELECT
u.name AS ‘사용자 이름’,
u.ubuntu_username AS ‘우분투 아이디’,
g.ubuntu_groupname AS ‘우분투 그룹 이름’,
dc.server_id AS ‘배정된 서버’,
(
SELECT
up.port_number
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use = ‘ssh’
) AS ‘ssh 포트’,
(
SELECT
up.port_number
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use = ‘jupyter notebook’
) AS ‘jupyter 포트’,
(
SELECT
GROUP_CONCAT (up.port_number) # 오류 : 빨간색 삭제
FROM
used_ports up
WHERE
up.docker_container_record_id = dc.id
AND up.purpose_of_use != ‘ssh’
AND up.purpose_of_use != ‘jupyter notebook’
) AS ‘할당된 다른 포트’,
dc.expiring_at AS ‘사용 만료일’,
dc.created_by AS ‘컨테이너 생성한 관리자’,
dc.created_at AS ‘컨테이너 생성 일자’,
dc.image AS ‘컨테이너 이미지’,
dc.image_version AS ‘컨테이너 버전’,
dc.container_name AS ‘컨테이너 이름’,
u.note AS ‘노트’
FROM
user u
LEFT JOIN `group` g ON u.ubuntu_gid = g.ubuntu_gid
JOIN docker_container dc ON u.id = dc.user_id
WHERE
dc.existing = TRUE
ORDER BY
dc.server_id ASC,
u.name ASC;