글작성하기 테스크_구글docs에서 복붙

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;

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다