Chapter 18. 환경변수 & 셸 설정
환경변수
환경변수(Environment Variable)는 운영체제가 프로세스에게 전달하는 이름=값 형태의 설정 정보이다. 프로세스는 환경변수를 통해 자신이 실행되는 환경에 대한 정보를 얻는다.
환경변수는 리눅스뿐만 아니라 모든 운영체제에 존재하는 개념이며, 애플리케이션 설정을 코드와 분리하는 핵심 메커니즘이다.
셸 변수 vs 환경변수
이 둘은 자주 혼동되지만 범위가 다르다:
- 셸 변수: 현재 셸 안에서만 유효하다. 자식 프로세스에 전달되지 않는다.
- 환경변수:
export로 내보내면 자식 프로세스에도 상속된다.
# 셸 변수 (현재 셸에서만 유효)
MY_VAR="hello"
echo $MY_VAR # hello
bash -c 'echo $MY_VAR' # (빈 출력 — 자식 셸에 전달되지 않음)
# 환경변수 (자식 프로세스에도 전달)
export MY_VAR="hello"
echo $MY_VAR # hello
bash -c 'echo $MY_VAR' # hello (자식 셸에서도 접근 가능)
환경변수 확인 및 설정
# 모든 환경변수 출력
env
printenv
# 특정 환경변수 확인
echo $HOME
echo $PATH
echo $USER
printenv HOME
# 현재 셸의 모든 변수 (환경변수 + 셸 변수 + 함수)
set
설정 및 해제
# 환경변수 설정 (현재 셸 + 자식 프로세스)
export DB_HOST="localhost"
export DB_PORT=5432
# 한 줄로 설정과 export 동시에
export APP_ENV="production"
# 변수 해제 (삭제)
unset DB_HOST
# 특정 명령어에만 일시적으로 환경변수 설정 ★
DB_HOST=remotedb APP_ENV=staging ./myapp
# myapp은 DB_HOST=remotedb로 실행되지만, 현재 셸에는 영향 없음
주요 시스템 환경변수
| 변수 | 설명 | 예시 |
|---|---|---|
HOME |
현재 사용자의 홈 디렉터리 | /home/devops |
USER |
현재 사용자 이름 | devops |
SHELL |
현재 사용자의 기본 셸 | /bin/bash |
PATH |
실행 파일 검색 경로 | /usr/local/bin:/usr/bin:/bin |
PWD |
현재 작업 디렉터리 | /home/devops/projects |
LANG |
시스템 로캘(언어) 설정 | en_US.UTF-8 |
TERM |
터미널 종류 | xterm-256color |
EDITOR |
기본 텍스트 편집기 | vim |
HOSTNAME |
호스트명 | web-server-01 |
UID |
현재 사용자 ID | 1000 |
PATH — 가장 중요한 환경변수
PATH는 셸이 명령어를 찾을 때 검색하는 디렉터리 목록이다.:으로 구분된 디렉터리 경로의 나열이며, 왼쪽부터 순서대로 검색한다.
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# ls를 실행하면 셸이 다음 순서로 검색:
# 1. /usr/local/sbin/ls → 없음
# 2. /usr/local/bin/ls → 없음
# 3. /usr/sbin/ls → 없음
# 4. /usr/bin/ls → 있음! → 실행
PATH에 경로 추가
# PATH 끝에 추가 (기존 PATH 뒤에)
export PATH="$PATH:/opt/myapp/bin"
# PATH 앞에 추가 (우선순위 높게)
export PATH="/opt/myapp/bin:$PATH"
앞에 추가 vs 뒤에 추가:
- 앞에 추가하면 시스템 기본 명령어보다 우선 실행된다
- 뒤에 추가하면 시스템 기본 명령어를 그대로 사용하되, 새 경로는 보조로 사용된다
- 보안상 일반적으로 뒤에 추가하는 것이 안전하다
"command not found" 문제 해결
# 1. 명령어가 어디에 있는지 확인
which python3
type python3
# 2. 명령어가 설치되어 있는지 확인
find / -name "python3" -type f 2>/dev/null
# 3. 찾은 경로가 PATH에 있는지 확인
echo $PATH | tr ':' '\n' | grep "/usr/local/bin"
# 4. 없으면 PATH에 추가
export PATH="$PATH:/found/path"
히스토리 관리
bash는 실행한 명령어를 ~/.bash_history 파일에 저장한다.
# 히스토리 확인
history
history 20 # 최근 20개
# 히스토리에서 검색
history | grep "docker"
# 이전 명령어 재실행
!! # 직전 명령어 재실행
!123 # 히스토리 번호 123 재실행
!docker # "docker"로 시작하는 가장 최근 명령어 재실행
# 역방향 검색 (가장 유용!) ★★★
# Ctrl + R → 검색어 입력 → 매치되면 Enter로 실행
# Ctrl + R을 반복 누르면 이전 매치로 이동
보안: 히스토리에 민감한 정보 남기지 않기
# 방법 1: 명령어 앞에 공백을 넣으면 히스토리에 기록되지 않음 (HISTCONTROL=ignorespace 필요)
export DB_PASSWORD="secret123" # 맨 앞에 공백
# 방법 2: 환경변수 파일에서 읽기
source /etc/myapp/env # 파일에서 로드
# 방법 3: 현재 세션의 히스토리 삭제
history -c && history -w
Chapter 19. 사용자 & 그룹 관리
리눅스의 사용자 체계
사용자는 크게 세 가지로 분류된다
| 종류 | UID 범위 | 설명 |
|---|---|---|
| root | 0 | 시스템 최고 관리자. 모든 권한을 가진다. |
| 시스템 사용자 | 1~999 | 서비스(데몬)를 실행하기 위한 계정. 직접 로그인하지 않는다. nginx, mysql, www-data 등. |
| 일반 사용자 | 1000~ | 사람이 로그인하여 사용하는 계정. |
UID(User ID) 범위는 배포판마다 약간 다를 수 있지만, 1000부터 일반 사용자가 시작되는 것은 대부분 공통이다.
사용자 정보가 저장되는 파일
/etc/passwd — 사용자 계정 정보
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
devops:x:1000:1000:DevOps User:/home/devops:/bin/bash
nginx:x:33:33:nginx,,,:/nonexistent:/usr/sbin/nologin
devops : x : 1000 : 1000 : DevOps User : /home/devops : /bin/bash
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ 로그인 셸
│ │ │ │ │ └─ 홈 디렉터리
│ │ │ │ └─ 코멘트 (이름, 설명)
│ │ │ └─ GID (기본 그룹 ID)
│ │ └─ UID (사용자 ID)
│ └─ 비밀번호 (x = /etc/shadow에 저장)
└─ 사용자명
/etc/shadow — 비밀번호 해시 (root만 읽기 가능)
$ sudo cat /etc/shadow
devops:$6$rounds=656000$salt...$hash...:19400:0:99999:7:::
| 필드 | 의미 |
|---|---|
| 1 | 사용자명 |
| 2 | 비밀번호 해시 (! 또는 *면 로그인 불가) |
| 3 | 마지막 비밀번호 변경일 (1970-01-01부터의 일수) |
| 4 | 최소 변경 간격 (일) |
| 5 | 최대 유효 기간 (일) |
| 6 | 만료 경고 기간 (일) |
| 7 | 비활성화 기간 (일) |
| 8 | 계정 만료일 |
/etc/group — 그룹 정보
$ cat /etc/group
devteam:x:1001:devops,deploy,alice
docker:x:999:devops
devteam : x : 1001 : devops,deploy,alice
│ │ │ │
│ │ │ └─ 그룹 멤버 목록
│ │ └─ GID (그룹 ID)
│ └─ 그룹 비밀번호 (거의 사용하지 않음)
└─ 그룹명
사용자 관리 명령어
useradd — 사용자 생성
# 기본 사용자 생성
sudo useradd devops
# 실무에서 사용하는 전형적인 옵션 조합 ★
sudo useradd -m -s /bin/bash -c "DevOps Engineer" devops
# -m : 홈 디렉터리 자동 생성 (/home/devops)
# -s : 로그인 셸 지정
# -c : 코멘트 (사용자 설명)
# 비밀번호 설정
sudo passwd devops
# 특정 UID/GID 지정
sudo useradd -m -u 1500 -g devteam -s /bin/bash deploy
# 시스템 사용자 생성 (서비스용, 로그인 불가)
sudo useradd -r -s /usr/sbin/nologin -d /nonexistent myservice
# -r : 시스템 사용자 (UID < 1000)
# -s /usr/sbin/nologin : 로그인 차단
# -d /nonexistent : 홈 디렉터리 없음
# 사용자 생성 + sudo 그룹에 추가 (Ubuntu)
sudo useradd -m -s /bin/bash -G sudo newadmin
sudo passwd newadmin
useradd vs adduser:
useradd: 저수준 명령어. 옵션을 직접 지정해야 한다.adduser: Debian/Ubuntu에서 제공하는 대화형 래퍼 스크립트. 비밀번호 설정, 홈 디렉터리 생성 등을 자동으로 처리한다.
usermod — 사용자 수정
# 사용자를 추가 그룹에 추가 ★★★
sudo usermod -aG docker devops
# -a : append (기존 그룹 유지하면서 추가)
# -G : 보조 그룹 지정
# ⚠️ -a를 빼먹으면 기존 보조 그룹이 모두 제거된다!
sudo usermod -G docker devops # 위험: docker만 남고 나머지 그룹 제거
# 로그인 셸 변경
sudo usermod -s /bin/zsh devops
# 사용자명 변경
sudo usermod -l newname oldname
# 홈 디렉터리 변경 (기존 파일도 이동)
sudo usermod -d /new/home -m devops
# 계정 잠금 / 해제
sudo usermod -L devops # Lock (비밀번호 앞에 ! 추가)
sudo usermod -U devops # Unlock
그룹 관리
모든 사용자는 기본 그룹(primary group)과 0개 이상의 보조 그룹(supplementary groups)을 가진다.
- 기본 그룹: 사용자가 파일을 생성할 때 적용되는 그룹.
/etc/passwd의 GID 필드. - 보조 그룹: 추가적인 권한을 부여하기 위한 그룹.
/etc/group에 멤버로 등록.
# 현재 사용자의 그룹 정보 확인
id
# uid=1000(devops) gid=1000(devops) groups=1000(devops),27(sudo),999(docker)
# 특정 사용자의 그룹 확인
id deploy
groups deploy
그룹 관리 명령어
# 그룹 생성
sudo groupadd devteam
sudo groupadd -g 2000 devteam # GID 지정
# 그룹에 사용자 추가 ★
sudo usermod -aG devteam devops
sudo usermod -aG devteam alice
sudo usermod -aG devteam deploy
# 그룹에서 사용자 제거
sudo gpasswd -d alice devteam
# 그룹 삭제
sudo groupdel devteam
# 그룹명 변경
sudo groupmod -n newname oldname
그룹 변경 즉시 적용
usermod -aG로 그룹을 추가해도 현재 세션에는 즉시 반영되지 않는다. 로그아웃/로그인을 해야 적용된다.
# 그룹 추가 후 확인
sudo usermod -aG docker devops
groups # 아직 docker가 안 보일 수 있음
# 방법 1: 로그아웃 후 재로그인 (가장 확실)
# 방법 2: newgrp으로 임시 적용
newgrp docker
groups # 이제 docker가 보임
그룹 활용
Docker 그룹
Docker를 sudo 없이 사용하려면 사용자를 docker 그룹에 추가해야 한다.
sudo usermod -aG docker devops
# 로그아웃 후 재로그인
docker ps # sudo 없이 실행 가능
sudo 그룹
# Ubuntu: sudo 그룹에 추가
sudo usermod -aG sudo newadmin
# RHEL/Fedora: wheel 그룹에 추가
sudo usermod -aG wheel newadmin
서비스 전용 사용자 생성
애플리케이션은 전용 사용자로 실행하는 것이 보안 best practice이다.
root로 실행하면 취약점 발생 시 시스템 전체가 위험해진다.
# 서비스 전용 사용자 생성
sudo useradd -r -s /usr/sbin/nologin -d /opt/myapp -c "MyApp service" myapp
# 애플리케이션 디렉터리 소유자 설정
sudo chown -R myapp:myapp /opt/myapp
# systemd 서비스에서 해당 사용자로 실행
# [Service]
# User=myapp
# Group=myapp
사용자 전환
su — Switch User
# root로 전환 (root 비밀번호 필요)
su -
# 특정 사용자로 전환
su - deploy
# deploy 사용자의 비밀번호 입력
# 현재 환경 유지하면서 전환 (- 없이)
su deploy
# 환경변수, 작업 디렉터리가 변경되지 않음
# 명령어 하나만 다른 사용자로 실행
su - deploy -c "whoami"
su - vs su (하이픈의 차이):
su -(또는su -l,su --login): 대상 사용자의 로그인 셸을 완전히 재현한다. 환경변수, HOME, PATH 등이 대상 사용자의 것으로 설정된다.su(하이픈 없이): 사용자만 전환하고, 현재 환경을 유지한다. PATH가 원래 사용자의 것이라서 혼란이 생길 수 있다.
su -를 사용하는 것이 권장된다.
sudo su - vs sudo -i
# 둘 다 root 셸로 전환 (sudo 비밀번호 사용)
sudo su -
sudo -i
# 차이점은 미미하지만, sudo -i가 더 직접적이고 감사 로그에 더 잘 남는다
사용자 정보 확인 명령어
# 현재 사용자 확인
whoami
# devops
# 현재 로그인한 모든 사용자
who
# devops pts/0 2025-03-23 10:00 (192.168.1.100)
# deploy pts/1 2025-03-23 11:30 (192.168.1.101)
# 더 자세한 정보
w
# USER TTY FROM LOGIN@ IDLE WHAT
# devops pts/0 192.168.1.100 10:00 0.00s vim config.yaml
# deploy pts/1 192.168.1.101 11:30 2:15 bash
# 사용자/그룹 ID 정보
id
id deploy
# 마지막 로그인 기록
last | head -20
last devops
# 마지막 로그인 실패 기록
sudo lastb | head -20
시나리오
새 팀원 온보딩
#!/bin/bash
USERNAME=$1
FULLNAME=$2
# 사용자 생성
sudo useradd -m -s /bin/bash -c "$FULLNAME" $USERNAME
# 임시 비밀번호 설정 + 다음 로그인 시 변경 강제
echo "${USERNAME}:TempPass123!" | sudo chpasswd
sudo chage -d 0 $USERNAME
# 필요한 그룹에 추가
sudo usermod -aG sudo $USERNAME
sudo usermod -aG docker $USERNAME
sudo usermod -aG devteam $USERNAME
# SSH 키 디렉터리 설정
sudo mkdir -p /home/$USERNAME/.ssh
sudo chmod 700 /home/$USERNAME/.ssh
sudo chown -R $USERNAME:$USERNAME /home/$USERNAME/.ssh
echo "User $USERNAME created successfully"
퇴사자 계정 처리
#!/bin/bash
USERNAME=$1
# 1. 계정 잠금
sudo usermod -L $USERNAME
# 2. 실행 중인 프로세스 종료
sudo pkill -u $USERNAME
# 3. crontab 제거
sudo crontab -r -u $USERNAME
# 4. 홈 디렉터리 백업
sudo tar -czf /backup/departed/${USERNAME}-$(date +%Y%m%d).tar.gz /home/$USERNAME
# 5. (확인 후) 계정 삭제
# sudo userdel -r $USERNAME
echo "Account $USERNAME has been locked and backed up"
Chapter 20. 패키지 관리
패키지 관리
패키지(Package)란 소프트웨어를 설치하기 위해 필요한 파일들(바이너리, 라이브러리, 설정 파일, 문서 등)을 하나로 묶은 것이다. 패키지 매니저는 이 패키지를 설치, 업데이트, 삭제하는 도구이다.
패키지 매니저가 없다면 소프트웨어를 설치할 때마다 소스 코드를 다운로드하고, 의존성 라이브러리를 직접 찾아 설치하고, 컴파일해야 한다. 패키지 매니저가 이 모든 과정을 자동화한다.
패키지 매니저의 역할
- 의존성 자동 해결: A 패키지가 B 라이브러리를 필요로 하면 B를 자동 설치
- 버전 관리: 패키지의 설치, 업그레이드, 다운그레이드
- 저장소 관리: 어디서 패키지를 다운로드할지
- 무결성 검증: 패키지의 서명을 확인하여 변조 방지
배포판별 패키지 시스템
| 배포판 계열 | 패키지 형식 | 저수준 도구 | 고수준 도구 |
|---|---|---|---|
| Debian/Ubuntu | .deb |
dpkg |
apt |
| RHEL/Fedora | .rpm |
rpm |
dnf (이전: yum) |
| Alpine | .apk |
— | apk |
저수준 vs 고수준
- 저수준(dpkg, rpm): 개별 패키지 파일을 직접 처리. 의존성을 자동 해결하지 않는다.
- 고수준(apt, dnf): 저장소에서 패키지를 검색하고, 의존성을 자동으로 해결한다. 실무에서는 거의 항상 고수준 도구를 사용한다.
APT — Debian/Ubuntu 패키지 관리
패키지 목록 업데이트
# 저장소에서 최신 패키지 목록을 가져온다 (설치는 아님)
sudo apt update
apt update는 무엇을 하는가: /etc/apt/sources.list에 정의된 저장소 서버에 접속하여 "어떤 패키지가 어떤 버전으로 이용 가능한지" 목록을 다운로드한다. 패키지를 설치/업그레이드하기 전에 반드시 먼저 실행해야 한다.
패키지 설치/삭제/업그레이드
# 패키지 설치
sudo apt install nginx
sudo apt install nginx curl vim # 여러 패키지 동시
# 확인 없이 설치 (-y)
sudo apt install -y nginx
# 패키지 삭제 (설정 파일은 유지)
sudo apt remove nginx
# 패키지 + 설정 파일까지 완전 삭제
sudo apt purge nginx
# 패키지 재설치
sudo apt reinstall nginx
# 모든 패키지 업그레이드
sudo apt update && sudo apt upgrade -y
# 의존성 변경이 필요한 업그레이드까지 포함 (커널 등)
sudo apt full-upgrade
# 불필요한 의존성 패키지 자동 제거
sudo apt autoremove
패키지 검색 및 정보 확인
# 패키지 검색
apt search nginx
apt search "web server"
# 패키지 상세 정보
apt show nginx
# 설치된 패키지 목록
apt list --installed
apt list --installed | grep nginx
# 업그레이드 가능한 패키지 목록
apt list --upgradable
# 패키지가 설치한 파일 목록
dpkg -L nginx
# 특정 파일이 어느 패키지에 포함되어 있는지
dpkg -S /usr/sbin/nginx
# nginx-core: /usr/sbin/nginx
패키지 버전 고정 (Version Pinning)
자동 업그레이드로 인해 호환성이 깨지는 것을 방지하려면 특정 패키지의 버전을 고정할 수 있다.
APT (Debian/Ubuntu)
# 패키지 버전 고정 (hold)
sudo apt-mark hold nginx
# 고정 해제
sudo apt-mark unhold nginx
# 고정된 패키지 목록 확인
apt-mark showhold
# 특정 버전 설치
sudo apt install nginx=1.18.0-0ubuntu1
핵심 원칙
- 항상 고수준 도구(apt, dnf)를 우선 사용한다
apt install전에 반드시apt update를 먼저 실행한다- 프로덕션 서버에서는 버전 고정을 고려한다
- 자동 보안 업데이트를 활성화한다
- 소스 설치는 최후의 수단으로만 사용한다
Chapter 21. 네트워킹
IP 주소
IP 주소는 네트워크에서 장치를 식별하는 주소이다.
- IPv4:
192.168.1.100— 32비트, 약 43억 개. 현재 주류. - IPv6:
2001:db8::1— 128비트, 사실상 무한. 점진적 전환 중. - 루프백(Loopback):
127.0.0.1(localhost) — 자기 자신을 가리키는 특수 주소. - 사설 IP: 내부 네트워크에서만 사용. 인터넷에서 직접 접근 불가.
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
- 공인 IP: 인터넷에서 직접 접근 가능한 주소.
포트 (Port)
IP 주소가 건물 주소라면, 포트는 방 번호이다. 하나의 IP에서 여러 서비스를 구분하는 번호(0~65535)이다.
| 포트 | 서비스 |
|---|---|
| 22 | SSH |
| 80 | HTTP |
| 443 | HTTPS |
| 3306 | MySQL |
| 5432 | PostgreSQL |
| 6379 | Redis |
| 8080 | 대체 HTTP (개발용) |
| 27017 | MongoDB |
- Well-known 포트 (0~1023): 시스템/표준 서비스용. 바인딩에 root 권한 필요.
- 등록된 포트 (1024~49151): 일반 애플리케이션용.
- 동적 포트 (49152~65535): 임시 연결에 자동 할당.
DNS (Domain Name System)
사람이 읽기 쉬운 도메인 이름(google.com)을 IP 주소(142.250.196.110)로 변환하는 시스템이다.
브라우저: "google.com에 접속하고 싶어"
↓ DNS 쿼리
DNS 서버: "google.com은 142.250.196.110이야"
↓
브라우저: 142.250.196.110에 연결
# DNS 설정 파일
cat /etc/resolv.conf
# nameserver 8.8.8.8
# nameserver 8.8.4.4
# 로컬 호스트 이름 매핑 (DNS보다 우선)
cat /etc/hosts
# 127.0.0.1 localhost
# 192.168.1.10 db-server
# 192.168.1.20 cache-server
TCP vs UDP
| 특성 | TCP | UDP |
|---|---|---|
| 연결 방식 | 연결 지향 (3-way handshake) | 비연결 |
| 신뢰성 | 데이터 전달 보장, 순서 보장 | 보장 없음 |
| 속도 | 상대적으로 느림 | 빠름 |
| 용도 | HTTP, SSH, DB 연결 | DNS, 스트리밍, 게임 |
CIDR 표기법
192.168.1.0/24와 같은 표기를 CIDR(Classless Inter-Domain Routing)이라 한다. /24는 네트워크 부분의 비트 수이다.
192.168.1.0/24
→ 네트워크: 192.168.1.0
→ 호스트 범위: 192.168.1.1 ~ 192.168.1.254
→ 사용 가능 IP: 254개
10.0.0.0/16
→ 호스트 범위: 10.0.0.1 ~ 10.0.255.254
→ 사용 가능 IP: 65,534개
/32 → 단일 IP (호스트 1개)
/0 → 모든 IP
네트워크 인터페이스 확인
ip — 현대적 네트워크 도구
ifconfig는 더 이상 권장되지 않으며(deprecated), ip 명령어가 표준이다.
# 모든 인터페이스와 IP 주소 확인 ★★★
ip addr show
ip a # 축약
# 특정 인터페이스만
ip addr show eth0
# 인터페이스 활성화/비활성화
sudo ip link set eth0 up
sudo ip link set eth0 down
# IP 주소 추가/삭제
sudo ip addr add 192.168.1.100/24 dev eth0
sudo ip addr del 192.168.1.100/24 dev eth0
# 라우팅 테이블 확인 ★
ip route show
ip r
# default via 10.0.0.1 dev eth0 ← 기본 게이트웨이
# 10.0.0.0/24 dev eth0 proto kernel ← 로컬 네트워크
연결 진단 명령어
ping — 기본 연결 확인
# 호스트 연결 확인
ping google.com
ping 8.8.8.8
# 횟수 지정 (-c)
ping -c 4 google.com
# 타임아웃 지정 (-W: 초)
ping -c 1 -W 3 10.0.0.1
ping이 실패하면: 네트워크 자체가 끊어졌거나, 방화벽이 ICMP를 차단하고 있거나, DNS가 동작하지 않는 경우이다.
curl — HTTP 요청 테스트
웹 서비스의 응답을 확인하는 데 가장 많이 사용한다.
# 기본 GET 요청
curl http://localhost:8080
# 응답 헤더 포함 (-i)
curl -i http://localhost:8080
# 상태 코드만 확인 (-o /dev/null -s -w)
curl -o /dev/null -s -w "%{http_code}\n" http://localhost:8080
# 200
# 상세 연결 과정 확인 (-v: verbose) ★
curl -v https://example.com
# POST 요청
curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://localhost:8080/api
# 연결 타임아웃 설정
curl --connect-timeout 5 --max-time 10 http://example.com
# SSL 인증서 무시 (개발용)
curl -k https://self-signed.example.com
dig / nslookup — DNS 조회
# DNS 조회
dig google.com
dig google.com +short # IP만 간결하게
# 142.250.196.110
# 특정 DNS 서버로 조회
dig @8.8.8.8 google.com
# 역방향 DNS (IP → 도메인)
dig -x 8.8.8.8
# 레코드 타입 지정
dig google.com MX # 메일 서버
dig google.com NS # 네임 서버
dig google.com CNAME # 별칭
dig google.com TXT # TXT 레코드
# nslookup (간단한 조회)
nslookup google.com
nslookup google.com 8.8.8.8
traceroute / tracepath — 경로 추적
패킷이 목적지까지 어떤 경로를 거치는지 추적한다.
# 경로 추적
traceroute google.com
# tracepath (traceroute 대안, 별도 설치 불필요한 경우 많음)
tracepath google.com
포트 & 연결 확인
ss — 소켓 통계
netstat의 현대적 대체 도구이다. 열린 포트, 연결 상태를 확인한다.
# 리스닝 중인 TCP 포트 확인 ★★★
ss -tlnp
# -t: TCP
# -l: LISTEN 상태만
# -n: 포트 번호를 숫자로 표시 (이름 변환 안 함)
# -p: 프로세스 정보 표시
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=2345))
# LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234))
# UDP 포트 확인
ss -ulnp
# 모든 TCP 연결 (LISTEN + ESTABLISHED + ...)
ss -tanp
# ESTABLISHED 연결만
ss -tanp state established
# 특정 포트로 필터링
ss -tlnp | grep :80
ss -tlnp sport = :443
netstat — 전통적 네트워크 통계
ss와 유사하지만, 일부 환경에서는 여전히 netstat이 기본 설치되어 있다.
# 설치 (없는 경우)
sudo apt install net-tools
# 리스닝 중인 TCP 포트
netstat -tlnp
# 모든 연결
netstat -tanp
파일 전송
wget — 파일 다운로드
# 파일 다운로드
wget https://example.com/file.tar.gz
# 다른 이름으로 저장
wget -O output.tar.gz https://example.com/file.tar.gz
# 백그라운드 다운로드
wget -b https://example.com/large-file.iso
# 재시도 횟수
wget --tries=3 https://example.com/file.tar.gz
# 이어받기 (중단된 다운로드 재개)
wget -c https://example.com/large-file.iso
curl vs wget
| 기능 | curl | wget |
|---|---|---|
| 파일 다운로드 | curl -O url |
wget url |
| API 호출 | ✓ (기본 용도) | ✗ |
| HTTP 메서드 지원 | GET, POST, PUT, DELETE... | GET만 |
| 재귀적 다운로드 | ✗ | ✓ (wget -r) |
| 이어받기 | curl -C - |
wget -c |
일반적으로: API 테스트는 curl, 파일 다운로드는 wget.
네트워크 트러블슈팅 체계
"서비스에 접속이 안 된다"는 보고가 왔을 때의 체계적 진단 순서:
# 1. 네트워크 인터페이스 확인 — IP가 할당되어 있는가?
ip addr show
# 2. 기본 게이트웨이 확인
ip route show
# 3. 외부 연결 확인 — 인터넷에 나갈 수 있는가?
ping -c 2 8.8.8.8
# 4. DNS 확인 — 도메인이 해석되는가?
dig example.com +short
# 실패하면: /etc/resolv.conf 확인, DNS 서버 변경
# 5. 특정 호스트+포트 연결 확인
curl -v http://target-host:8080
# 또는
ss -tlnp | grep :8080 # (대상 서버에서) 서비스가 리스닝 중인가?
# 6. 방화벽 확인
sudo ufw status # 또는 firewall-cmd --list-all
# 클라우드: 보안 그룹 규칙 확인
# 7. 서비스 상태 확인
systemctl status nginx
journalctl -u nginx -n 20
요약
| 명령어 | 용도 | 가장 많이 쓰는 형태 |
|---|---|---|
ip addr |
IP/인터페이스 확인 | ip a |
ip route |
라우팅 테이블 | ip r |
ping |
기본 연결 확인 | ping -c 4 host |
curl |
HTTP 요청/API 테스트 | curl -v, curl -s -o /dev/null -w "%{http_code}" |
dig |
DNS 조회 | dig domain +short |
ss |
포트/연결 확인 | ss -tlnp |
traceroute |
경로 추적 | |
wget |
파일 다운로드 | wget -O file url |
Chapter 22. SSH & 원격 접속
SSH
SSH(Secure Shell)는 네트워크를 통해 다른 컴퓨터에 안전하게 접속하기 위한 프로토콜이다. 모든 통신이 암호화되므로, 비밀번호나 명령어가 네트워크에서 평문으로 노출되지 않는다.
SSH 이전에는 telnet, rsh 같은 프로토콜을 사용했는데, 이들은 데이터를 암호화하지 않아서 도청에 취약했다. SSH는 이 문제를 완전히 해결했으며, 현재 서버 원격 접속의 사실상 유일한 표준이다.
SSH의 용도
- 원격 서버에 로그인하여 명령어 실행
- 서버 간 파일 전송 (scp, sftp, rsync)
- 포트 포워딩 (터널링)
- Git 원격 저장소 접근
- 자동화 스크립트에서 원격 명령어 실행
SSH의 동작 방식
SSH 연결은 두 단계로 이루어진다:
① 서버 인증: 클라이언트가 "이 서버가 진짜 내가 접속하려는 서버인가"를 확인한다. 서버의 공개 키(host key)를 이전에 저장해둔 것(~/.ssh/known_hosts)과 비교한다.
② 사용자 인증: 서버가 "이 사용자가 접속 권한이 있는가"를 확인한다. 비밀번호 또는 키 기반 인증을 사용한다.
키 기반 인증 (Key-Based Authentication)
비밀번호 인증의 문제점:
- 무차별 대입 공격에 취약
- 여러 서버에 비밀번호를 기억하기 어려움
- 자동화 스크립트에 비밀번호를 하드코딩하면 보안 위험
키 기반 인증은:
- 수학적으로 매우 강력한 보안 (2048/4096비트 RSA, Ed25519)
- 비밀번호 없이 자동화 가능
- AWS, GCP, Azure 등 클라우드 서비스의 기본 인증 방식
공개 키 / 개인 키
SSH 키는 한 쌍(pair)으로 생성된다:
- 개인 키(Private Key): 자신만 가지고 있는 비밀 키. 절대 공유하면 안 된다.
~/.ssh/id_rsa또는~/.ssh/id_ed25519 - 공개 키(Public Key): 서버에 등록하는 키. 공유해도 안전하다.
~/.ssh/id_rsa.pub또는~/.ssh/id_ed25519.pub
인증 흐름:
1. 사용자가 서버에 접속 시도
2. 서버가 랜덤 챌린지를 보냄
3. 클라이언트가 개인 키로 챌린지에 서명하여 응답
4. 서버가 등록된 공개 키로 서명을 검증
5. 검증 성공 → 접속 허용
비유하자면: 공개 키는 자물쇠, 개인 키는 열쇠이다. 자물쇠(공개 키)는 서버에 설치하고, 열쇠(개인 키)는 자기만 가지고 다닌다.
SSH 키 생성
# Ed25519 키 생성 (현재 추천되는 알고리즘) ★★★
ssh-keygen -t ed25519 -C "devops@example.com"
# -t: 알고리즘 타입
# -C: 코멘트 (보통 이메일, 식별용)
# 실행하면 다음을 물어본다:
# 1. 키 저장 경로 (기본: ~/.ssh/id_ed25519) → Enter
# 2. 패스프레이즈(비밀번호) → 보안을 위해 설정 권장, 자동화 시 빈 값
# RSA 키 생성 (레거시 시스템 호환 필요 시)
ssh-keygen -t rsa -b 4096 -C "devops@example.com"
# -b 4096: 키 길이 (최소 2048, 4096 권장)
# 결과:
# ~/.ssh/id_ed25519 ← 개인 키 (절대 유출 금지)
# ~/.ssh/id_ed25519.pub ← 공개 키 (서버에 등록)
서버에 공개 키 등록
# 방법 1: ssh-copy-id (가장 간편) ★
ssh-copy-id user@remote-server
# 비밀번호를 한 번 입력하면 공개 키가 서버의 ~/.ssh/authorized_keys에 추가된다
# 방법 2: 수동 등록
cat ~/.ssh/id_ed25519.pub | ssh user@remote-server "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# 방법 3: 클라우드 (AWS EC2)
# 인스턴스 생성 시 키 페어를 지정하면 자동으로 등록된다
# 또는 EC2 Instance Connect, Session Manager 사용
키 파일 권한 (필수!)
SSH는 키 파일의 권한이 너무 열려있으면 접속을 거부한다.
chmod 700 ~/.ssh/
chmod 600 ~/.ssh/id_ed25519 # 개인 키
chmod 644 ~/.ssh/id_ed25519.pub # 공개 키
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/config
SSH 접속
# 기본 접속
ssh user@hostname
ssh user@192.168.1.100
# 포트 지정 (기본: 22)
ssh -p 2222 user@hostname
# 특정 키 파일 지정
ssh -i ~/.ssh/my-key.pem ubuntu@ec2-xx-xx-xx-xx.compute-1.amazonaws.com
# 원격 서버에서 명령어 하나만 실행하고 종료
ssh user@server "uptime"
ssh user@server "df -h && free -h"
첫 접속 시 fingerprint 확인
처음 접속하는 서버에는 다음 메시지가 뜬다:
The authenticity of host 'server (192.168.1.100)' can't be established.
ED25519 key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
yes를 입력하면 서버의 공개 키가 ~/.ssh/known_hosts에 저장된다. 이후 접속 시에는 이 키를 비교하여 서버 위조(중간자 공격)를 방지한다.
SSH 설정 파일 (~/.ssh/config)
여러 서버에 접속할 때 매번 긴 명령어를 입력하는 대신, 설정 파일에 별칭을 등록할 수 있다.
# ~/.ssh/config
Host web-prod
HostName 10.0.1.100
User deploy
Port 22
IdentityFile ~/.ssh/prod-key.pem
Host web-staging
HostName 10.0.2.100
User deploy
IdentityFile ~/.ssh/staging-key.pem
Host db-prod
HostName 10.0.1.200
User admin
IdentityFile ~/.ssh/prod-key.pem
# 패턴 매칭 — 모든 호스트에 적용
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
AddKeysToAgent yes
# 설정 후에는 별칭으로 간단히 접속
ssh web-prod # ssh -i ~/.ssh/prod-key.pem deploy@10.0.1.100 과 동일
ssh web-staging
ssh db-prod
파일 전송
scp — 파일 복사
Secure Copy의 약자이다. SSH를 통해 파일을 복사한다.
# 로컬 → 원격
scp file.txt user@server:/home/user/
# 원격 → 로컬
scp user@server:/var/log/app.log ./
# 디렉터리 복사 (-r)
scp -r ./project/ user@server:/opt/
# 특정 포트
scp -P 2222 file.txt user@server:/tmp/
# SSH 설정 파일의 별칭 사용
scp file.txt web-prod:/opt/deploy/
Chapter 23. 셸 스크립트 기초
셸 스크립트
셸 스크립트(Shell Script)는 셸 명령어들을 파일에 순서대로 작성하여 자동으로 실행되게 하는 프로그램이다. 지금까지 배운 모든 명령어(grep, awk, find, systemctl 등)를 조합하여 복잡한 작업을 자동화할 수 있다.
셸 스크립트는 다음과 같은 곳에 사용된다:
- 서버 초기 설정 자동화
- 배포(deploy) 스크립트
- 백업 스크립트
- 모니터링 / 헬스체크
- CI/CD 파이프라인의 빌드/테스트 단계
- 크론 작업
첫 번째 스크립트
#!/bin/bash
# 첫 번째 셸 스크립트
echo "Hello, DevOps!"
echo "Current user: $(whoami)"
echo "Date: $(date)"
echo "Hostname: $(hostname)"
# 실행 권한 부여
chmod +x hello.sh
# 실행
./hello.sh
Shebang (#!)
스크립트 첫 줄의 #!/bin/bash를 Shebang(셔뱅)이라고 한다. 이 스크립트를 어떤 인터프리터로 실행할지 지정한다.
#!/bin/bash # bash로 실행
#!/bin/sh # POSIX sh로 실행 (더 이식성이 높음)
#!/usr/bin/env bash # PATH에서 bash를 찾아 실행 (이식성 좋음)
#!/usr/bin/python3 # Python 스크립트도 가능
#!/bin/bash vs #!/bin/sh: bash는 sh의 상위 호환이지만, bash 전용 기능(배열, [[ ]] 등)은 sh에서 동작하지 않는다. 이식성이 중요하면 #!/bin/sh, bash 기능이 필요하면 #!/bin/bash를 사용한다.
변수
변수 선언과 사용
#!/bin/bash
# 변수 선언 (= 양옆에 공백 금지!)
NAME="DevOps"
PORT=8080
LOG_DIR="/var/log/myapp"
# 변수 사용 ($변수명 또는 ${변수명})
echo "Hello, $NAME"
echo "Server running on port ${PORT}"
# 중괄호가 필요한 경우 — 변수명 뒤에 문자가 붙을 때
FILE="${NAME}_config.yaml" # DevOps_config.yaml
# $NAME_config로 쓰면 NAME_config라는 변수를 찾게 된다
특수 변수
| 변수 | 의미 |
|---|---|
$0 |
스크립트 이름 |
$1, $2... |
1번째, 2번째... 인자(argument) |
$# |
인자의 개수 |
$@ |
모든 인자 (개별 문자열로) |
$* |
모든 인자 (하나의 문자열로) |
$? |
직전 명령어의 종료 코드 ★ |
$$ |
현재 스크립트의 PID |
$! |
마지막 백그라운드 프로세스의 PID |
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "Total args: $#"
echo "All args: $@"
./script.sh hello world
# Script: ./script.sh
# First arg: hello
# Second arg: world
# Total args: 2
# All args: hello world
종료 코드 (Exit Code)
모든 명령어는 종료 시 종료 코드(exit code)를 반환한다. 0은 성공, 0이 아닌 값은 실패이다.
# 종료 코드 확인
ls /etc/passwd
echo $? # 0 (성공)
ls /nonexistent
echo $? # 2 (실패 — 파일 없음)
# 스크립트에서 종료 코드 지정
exit 0 # 성공
exit 1 # 실패
명령어 치환 (Command Substitution)
명령어의 출력 결과를 변수에 저장한다.
# $( ) 형태 (추천)
CURRENT_DATE=$(date +%Y%m%d)
FILE_COUNT=$(find /var/log -name "*.log" | wc -l)
MY_IP=$(hostname -I | awk '{print $1}')
# 백틱 형태 (레거시, 중첩 시 불편)
CURRENT_DATE=`date +%Y%m%d`
echo "Date: $CURRENT_DATE"
echo "Log files: $FILE_COUNT"
echo "IP: $MY_IP"
조건문
if 문
#!/bin/bash
if [ condition ]; then
# 조건이 참일 때
elif [ condition2 ]; then
# 조건2가 참일 때
else
# 모두 거짓일 때
fi
조건 표현식
파일 테스트:
| 표현식 | 의미 |
|---|---|
-f file |
파일이 존재하고 일반 파일인가 |
-d dir |
디렉터리가 존재하는가 |
-e path |
경로가 존재하는가 (파일/디렉터리 무관) |
-r file |
읽기 가능한가 |
-w file |
쓰기 가능한가 |
-x file |
실행 가능한가 |
-s file |
파일이 비어있지 않은가 (크기 > 0) |
문자열 비교:
| 표현식 | 의미 |
|---|---|
"$a" = "$b" |
같은가 |
"$a" != "$b" |
다른가 |
-z "$a" |
빈 문자열인가 (zero length) |
-n "$a" |
비어있지 않은가 (non-zero length) |
숫자 비교:
| 표현식 | 의미 |
|---|---|
$a -eq $b |
같은가 (equal) |
$a -ne $b |
다른가 (not equal) |
$a -gt $b |
큰가 (greater than) |
$a -lt $b |
작은가 (less than) |
$a -ge $b |
크거나 같은가 |
$a -le $b |
작거나 같은가 |
예시
#!/bin/bash
# 파일 존재 확인
CONFIG="/etc/myapp/config.yaml"
if [ -f "$CONFIG" ]; then
echo "Config found: $CONFIG"
else
echo "ERROR: Config not found!"
exit 1
fi
# 인자 확인
if [ $# -eq 0 ]; then
echo "Usage: $0 <environment>"
echo " environments: dev, staging, prod"
exit 1
fi
# 종료 코드로 성공/실패 판단
if systemctl is-active --quiet nginx; then
echo "Nginx is running"
else
echo "Nginx is NOT running"
sudo systemctl start nginx
fi
# 디스크 사용률 체크
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -gt 90 ]; then
echo "WARNING: Disk usage is ${USAGE}%"
fi
[[ ]] vs [ ]
[[ ]]는 bash 전용 확장이며, [ ]보다 강력하다:
# [[ ]]의 장점:
# 1. 패턴 매칭
[[ "$string" == *.log ]]
# 2. 정규표현식
[[ "$string" =~ ^[0-9]+$ ]]
# 3. && / || 사용 가능
[[ -f "$file" && -r "$file" ]]
# [ ]에서는 -a / -o를 사용해야 한다 (비추천)
[ -f "$file" -a -r "$file" ]
추천: bash 스크립트(#!/bin/bash)에서는 [[ ]]를 사용한다.
반복문
for 문
# 리스트 순회
for server in web-01 web-02 web-03; do
echo "Checking $server..."
ssh "$server" "uptime"
done
# 범위 (Brace Expansion)
for i in {1..10}; do
echo "Iteration $i"
done
# C 스타일
for ((i=0; i<5; i++)); do
echo "Count: $i"
done
# 명령어 결과 순회
for file in /var/log/*.log; do
echo "$(wc -l < "$file") lines in $file"
done
# 파일의 각 줄 처리
while IFS= read -r line; do
echo "Processing: $line"
done < servers.txt
while 문
# 기본 while
count=0
while [ $count -lt 5 ]; do
echo "Count: $count"
((count++))
done
# 서비스가 시작될 때까지 대기 ★
echo "Waiting for service to start..."
while ! curl -s http://localhost:8080/health > /dev/null 2>&1; do
echo " Not ready yet, retrying in 3s..."
sleep 3
done
echo "Service is up!"
# 파일 읽기 (줄 단위)
while IFS= read -r line; do
echo "$line"
done < /etc/hosts
until 문
while의 반대 — 조건이 참이 될 때까지 반복한다.
until systemctl is-active --quiet nginx; do
echo "Nginx not running, starting..."
sudo systemctl start nginx
sleep 2
done
echo "Nginx is running"
함수
#!/bin/bash
# 함수 정의
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
check_service() {
local service_name=$1 # local: 함수 내 지역 변수
if systemctl is-active --quiet "$service_name"; then
log "$service_name is running"
return 0
else
log "ERROR: $service_name is not running"
return 1
fi
}
# 함수 호출
log "Starting health check"
check_service nginx
check_service postgresql
# 반환값 활용
if check_service redis; then
log "Redis OK"
else
log "Redis FAILED — attempting restart"
sudo systemctl restart redis
fi
local 키워드: 함수 내에서 선언한 변수가 함수 밖에 영향을 주지 않게 한다. 사용하지 않으면 전역 변수가 되어 예기치 않은 버그가 발생할 수 있다.
에러 처리
set 옵션 — 안전한 스크립트의 시작
#!/bin/bash
set -euo pipefail
| 옵션 | 효과 |
|---|---|
set -e |
명령어가 실패(종료 코드 ≠ 0)하면 스크립트 즉시 종료 ★ |
set -u |
정의되지 않은 변수 사용 시 에러로 종료 ★ |
set -o pipefail |
파이프라인에서 중간 명령어가 실패해도 감지 |
# set -e 없이: 에러가 발생해도 다음 줄이 실행된다 (위험!)
rm -rf /opt/myapp/
cp -r /tmp/deploy/* /opt/myapp/ # rm이 실패해도 실행됨
# set -e 있으면: rm이 실패하면 스크립트가 즉시 멈춘다 (안전!)
trap — 종료 시 정리 작업
#!/bin/bash
set -euo pipefail
# 스크립트 종료 시 (정상/비정상 모두) 실행할 정리 코드
cleanup() {
echo "Cleaning up temporary files..."
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
TEMP_DIR=$(mktemp -d)
echo "Working in $TEMP_DIR"
# 작업 수행 (에러 발생해도 cleanup이 자동 호출된다)
cp important_file.txt "$TEMP_DIR/"
# ... 추가 작업 ...
스크립트 예시
배포 스크립트
#!/bin/bash
set -euo pipefail
# --- 설정 ---
APP_NAME="myapp"
DEPLOY_DIR="/opt/${APP_NAME}"
RELEASE_DIR="${DEPLOY_DIR}/releases"
CURRENT_LINK="${DEPLOY_DIR}/current"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
NEW_RELEASE="${RELEASE_DIR}/${TIMESTAMP}"
log() { echo "[$(date '+%H:%M:%S')] $1"; }
# --- 인자 확인 ---
ARTIFACT=${1:?"Usage: $0 <artifact-path>"}
[ -f "$ARTIFACT" ] || { log "ERROR: $ARTIFACT not found"; exit 1; }
# --- 배포 ---
log "Starting deployment..."
mkdir -p "$NEW_RELEASE"
tar -xzf "$ARTIFACT" -C "$NEW_RELEASE"
log "Switching symlink..."
ln -sfn "$NEW_RELEASE" "$CURRENT_LINK"
log "Restarting service..."
sudo systemctl restart "$APP_NAME"
# 이전 릴리스 정리 (최근 5개만 유지)
ls -dt ${RELEASE_DIR}/*/ | tail -n +6 | xargs rm -rf
log "Deployment completed: $TIMESTAMP"
서버 헬스체크
#!/bin/bash
set -euo pipefail
ALERT_EMAIL="ops@example.com"
CHECKS_FAILED=0
check() {
local name=$1
local cmd=$2
if eval "$cmd" > /dev/null 2>&1; then
echo " ✓ $name"
else
echo " ✗ $name FAILED"
((CHECKS_FAILED++))
fi
}
echo "=== Health Check: $(hostname) ==="
echo "Time: $(date)"
echo ""
# 서비스 체크
echo "[Services]"
check "Nginx" "systemctl is-active --quiet nginx"
check "PostgreSQL" "systemctl is-active --quiet postgresql"
check "Redis" "systemctl is-active --quiet redis"
# 리소스 체크
echo "[Resources]"
DISK=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
MEM=$(free | awk '/Mem:/ {printf "%.0f", $3/$2*100}')
check "Disk < 90%" "[ $DISK -lt 90 ]"
check "Memory < 90%" "[ $MEM -lt 90 ]"
# 결과
echo ""
if [ $CHECKS_FAILED -gt 0 ]; then
echo "RESULT: $CHECKS_FAILED check(s) FAILED"
exit 1
else
echo "RESULT: All checks passed"
exit 0
fi
셸 스크립트 작성 모범 사례
- 항상
set -euo pipefail로 시작한다 - 변수는 항상 따옴표로 감싼다:
"$VAR"(공백이 포함된 값 보호) - Shebang을 명시한다:
#!/bin/bash - 함수로 구조화한다: 재사용 가능하고 읽기 쉽다
- 에러 메시지는 stderr로:
echo "ERROR" >&2 - 임시 파일은
mktemp으로 생성하고,trap으로 정리한다 - 하드코딩 대신 변수/인자를 사용한다
- 주석을 충분히 작성한다
- 복잡한 로직은 Python 등 다른 언어를 고려한다
'Learning Log' 카테고리의 다른 글
| [멋사 클라우드 5기] Day 40 - Dockerfile (0) | 2026.04.06 |
|---|---|
| [멋사 클라우드 5기] Day 39 - Docker Image & Container (0) | 2026.04.06 |
| [멋사 클라우드 5기] Day 35 & 36 - Linux (2) (0) | 2026.03.25 |
| [멋사 클라우드 5기] Day 33 & 34 - Linux (1) (1) | 2026.03.25 |
| [멋사 클라우드 5기] Day 31 & 32 - OSI 7 Layers (1) | 2026.03.16 |