서버 상태 동기화 스크립트 기술 문서 작성

1. 배경 (Background)

당신은 GPU 서버 관리팀의 일원입니다. 최근 교내 정전 사태로 인해 서버 전체가 재부팅되는 사고가 있었습니다. 이 과정에서 Docker 컨테이너들이 재시작되었으나, DB에 저장된 설정 정보와 실제 구동 중인 컨테이너 상태 간에 데이터 불일치(Port, UID/GID 등)가 발생했습니다.

우선 수동으로 복구를 완료했으나, 향후 동일한 사태를 대비해 동료 A와 함께 자동화 스크립트(sync_containers.sh)를 작성하는 업무를 맡았습니다. 동료 A는 빠르게 스크립트를 작성하고, 당신에게 전달해주었습니다. 당신의 임무는 이 스크립트를 다른 동료도 쉽게 사용할 수 있도록 기술 문서를 작성하여 배포하는 것입니다.

2. 목표 (Objectives)

동료 A가 작성한 쉘 스크립트 코드를 분석하고, 다른 팀원들도 이 도구를 이해하고 사용할 수 있도록 하기 위한 기술 문서를 노션으로 작성하십시오.

3. 필수 포함 항목

제출하는 PDF 문서에는 다음의 내용이 반드시 포함되어야 합니다. (목차 구성은 자유입니다.)

  1. 스크립트 개요: 이 스크립트가 해결하고자 하는 문제와 핵심 동작 로직(Flow) 설명
  2. 사용 가이드:
    • 사용법 및 옵션 설명 (-dry-run 등)
  3. 코드 분석:
    • 주요 함수 또는 로직 블록별 기능 설명

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 코드를 이해하기 위한 추가자료

  • MySQL 테이블 및 뷰 정의 sql

— 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;

제공해주신 전체 스크립트를 기반으로 **완전한 기술 문서(노션에 바로 붙여넣기 가능한 형태)**를 작성해드리겠습니다.

📘 sync_containers.sh 기술 문서

GPU 서버 관리팀 내부용
Docker 컨테이너 상태 자동 동기화 스크립트

🧩 1. 개요 (Overview)

최근 정전으로 인한 서버 전체 재부팅 과정에서 Docker 컨테이너 런타임 상태
DB에 저장된 설정 정보(이미지 버전, 포트, UID/GID 등)불일치 현상이 발생했습니다.

이 문제를 해결하기 위해
DB를 기준으로 Docker 환경을 자동 동기화하는 도구인 sync_containers.sh를 개발했습니다.

🎯 2. 주요 기능 (Key Features)

✔ DB를 기준으로 컨테이너 정보 읽기

  • 컨테이너 이름
  • 이미지 / 버전
  • UID / GID
  • 할당된 포트 정보

✔ 서버의 실제 Docker 컨테이너와 비교

  • 이미지 버전 비교
  • UID/GID 비교
  • 포트 매핑 비교
  • 컨테이너 실행 여부 확인

✔ 불일치 발생 시

  • 컨테이너 자동 재생성(recreate)
  • 정지된 컨테이너 자동 재시작

✔ 추가 옵션

옵션설명
–dry-run실행하지 않고 어떤 작업이 수행되는지 출력만 함
–auto-deleteDB에 없는 ‘고아 컨테이너’를 서버에서 자동 삭제

⚙️ 3. 스크립트 구조 요약

1. 환경변수( DB 정보 / 옵션 ) 설정

2. 명령행 옵션 파싱

3. ~/.my.cnf 자동 생성 (MySQL 인증)

4. DB에서 컨테이너 메타데이터 로드

5. 서버 docker 상태 조회

6. DB 기준으로 다음 항목 점검

   – 컨테이너 없음 → 생성

   – 컨테이너 정지됨 → 시작

   – 이미지/UID/GID/Port mismatch → 재생성

7. 서버에는 있고 DB에는 없는 컨테이너 처리

8. 종료

🏗 4. 설치 방법 (Setup)

필요 패키지 설치

sudo apt install docker.io jq mariadb-client -y

스크립트 권한 부여

chmod +x sync_containers.sh

🚀 5. 실행 방법

5.1 기본 실행

./sync_containers.sh

5.2 드라이런(dry-run)

./sync_containers.sh –dry-run

5.3 DB에 없는 컨테이너 자동 삭제

./sync_containers.sh –auto-delete

두 옵션을 동시에 사용할 수도 있음:

./sync_containers.sh –dry-run –auto-delete

🔐 6. MySQL 자동 인증 설정

스크립트는 실행 시 다음 파일을 자동 생성하여
패스워드 없이 mysql 명령을 수행할 수 있게 합니다.

~/.my.cnf

[client]

user=nfs_user

password=nfs_password

host=192.168.2.11

port=3307

권한 자동 설정:

chmod 600 ~/.my.cnf

🧠 7. 동작 상세 (Detailed Logic)

🔍 7.1 DB에서 컨테이너 목록 조회

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;

DB의 정상 컨테이너 목록을 불러와 스크립트에서 순회 처리합니다.

🔍 7.2 서버 docker 상태 수집

docker ps -a –format “{{.Names}} {{.Status}}” > /tmp/docker_status.txt

컨테이너 이름 + 상태를 가져와 DB 정보와 비교합니다.

🔄 7.3 동기화 로직

✔ (1) DB에는 있지만 서버에는 없는 경우 → 생성

  1. DB에서 포트 목록 조회
  2. 사용 중인지 확인
  3. 포트 바인딩 옵션 생성
  4. 다음 명령 실행

docker run -dit \

    –name <cname> \

    -p <port-mapping> \

    -e USER_ID=<ubuntu_username> \

    -e UID=<uid> \

    -e GID=<gid> \

    dguailab/<image>:<version>

✔ (2) 서버에는 있으나 정지됨 → 자동 시작

docker start <cname>

✔ (3) 이미지/UID/GID/Port mismatch → 재생성

비교 항목

항목비교 방법
이미지docker inspect
UID/GID컨테이너 환경변수 비교
PortDocker inspect → jq로 파싱

mismatch 발생 시:

  1. 컨테이너 종료 + 삭제

docker rm -f <cname>

  1. DB 기준으로 새로 생성

🗑 8. 서버에는 있으나 DB에는 없는 컨테이너 처리

DB 목록
vs
서버 컨테이너 목록

을 비교하여 서버만 가지고 있는 컨테이너를 검사합니다.

기본 동작

  • 삭제하지 않고 경고 출력:

[WARN] Containers on server but not in DB:

<container-list>

–auto-delete 사용 시

docker rm -f <container>

📘 9. 전체 Flow Chart

[Start]

    ↓

[Load DB Containers]

    ↓

[Check Docker Runtime]

    ↓

● DB에 있고 서버에 없음? → Create

● 서버에 있으나 Exited? → Start

● 정보 mismatch? → Recreate

● 모두 정상? → OK 표시

    ↓

[Server-only Containers?]

    ↓

● auto-delete 옵션? → Delete

● 아니면 Warning 출력

    ↓

[End]

📄 10. 샘플 실행 로그

Dry-run 예시

[INFO] Using DB: nfs_db at 192.168.2.11:3307

[INFO] Options: dry-run=true, auto-delete=false

[INFO] Loaded 23 containers from DB.

[CREATE] user01_container (image=python:3.10, user=user01)

[DRY-RUN] docker run -dit -p 2201:22 …

[MISMATCH] UID/GID differs: DB=1001/1001, Actual=1000/1000

[DRY-RUN] Would recreate user02_container

[OK] user03_container is running and matches DB

[DONE] Sync completed.

🪪 11. 주의사항

  • 반드시 root 또는 docker 그룹에서 실행
  • Docker 이미지 dguailab/* must exist
  • DB 정보는 재부팅 후 변경되지 않도록 .my.cnf를 유지
  • 도중 포트 충돌 시 자동 skip
  • jq 필수 설치

🕒 12. 자동 실행 설정 (Cron)

매일 새벽 4시에 자동 점검:

crontab -e

0 4 * * * /path/sync_containers.sh >> /var/log/sync_containers.log 2>&1

🎉 문서 완성

해당 문서는 노션에 그대로 붙여넣으면 정리된 포맷으로 잘 표시됩니다.
필요하다면 표·색상·아이콘 추가한 강화 버전도 제작해드릴 수 있습니다.

추가로 다음도 제공 가능합니다:

  • Flowchart 이미지 SVG 생성
  • 스크립트 개선 버전 (에러 대응 강화)
  • 운영팀 교육용 요약본

원하시면 말씀해주세요!

위로 스크롤