重启之后什么都没起来
한국어 원문으로 표시합니다.
이 실습은 진짜 VM 에서 돕니다
우분투 24.04 VM 한 대입니다. systemd 가 PID 1 이고 쿠버네티스는 없습니다. 준비 단계가 작은 주문 조회 앱(/opt/orders-svc/app.py)을 깔고, 이전 담당자가 셸에서 nohup 으로 띄워 둔 상태를 재현해 둡니다. 채점은 VM 안의 에이전트가 하므로 재부팅하지 마세요 — 채점이 끊깁니다. 부팅 때 뜨는지는 enable 상태와 target 의존으로 판정합니다.
목표
고객 VM 에 앱을 전용 사용자·환경 파일·재시작 정책·부팅 target 을 갖춘 systemd 서비스로 설치하고, enable 누락·kill -9·daemon-reload 누락·환경 파일 교체가 각각 무엇을 바꾸는지 직접 확인한 뒤, 인계용 점검 스크립트로 끝낸다.
왜 중요한가
"설치했고 잘 돈다" 는 지금 떠 있다는 뜻일 뿐이다. 셸에서 띄운 프로세스는 서비스 관리자가 모르는 프로세스라 죽어도 아무도 되살리지 않고, 재부팅하면 아무도 띄우지 않는다. systemctl start 만 하고 enable 을 빠뜨려도 똑같다. 부팅 때 기동은 [Install] 섹션이 enable 때 만드는 심링크 하나에 달려 있고, 유닛 파일을 고친 뒤 daemon-reload 를 잊으면 systemd 는 옛 설정으로 계속 돈다. 이 차이들을 한 번씩 손으로 만들어 봐야 고객 현장에서 "왜 아무것도 안 떠요" 에 5분 안에 답한다.
예상 60분이다. 준비에 몇 분이 걸린다. 세션이 끝나면 VM 이 사라지니 남기고 싶은 파일은 종료 전에 따로 보관한다.
단계
- 8181 포트를 잡고 있는 이전 담당자의 프로세스를 찾아
/root/svc/legacy.txt에pid=·user=·cgroup=(/proc/<pid>/cgroup의 경로)·unit=(그 경로의 마지막 조각)·survives_reboot=(yes 또는 no) 다섯 줄로 적는다. - 로그인할 수 없는 시스템 계정
orders를 만들고,/var/lib/orders(소유자 orders, 750)와/etc/orders/orders.env(root:orders, 640)를 만든다. 환경 파일에는ORDERS_PORT=8181, 16자 이상의 새ORDERS_TOKEN,ORDERS_DATA_DIR=/var/lib/orders를 넣는다. /etc/systemd/system/orders.service를 쓴다(User=orders, EnvironmentFile, ExecStart=/usr/bin/python3 /opt/orders-svc/app.py, Restart=on-failure, Wants·After=network-online.target, WantedBy=multi-user.target). daemon-reload 뒤 시작하면 실패한다. journal 에서 앱이 남긴 원인 줄(bind failed pid=…)을 찾아/root/svc/why-failed.txt에 그대로 옮긴다.- 이전 담당자의 프로세스를 내리고 orders.service 를 기동한다.
curl http://127.0.0.1:8181/healthz가 orders 사용자와 서비스의 주 프로세스 PID 로 응답해야 한다. - 서비스를 enable 하고 그때 나온 출력(만들어진 심링크 경로 포함)을
/root/svc/enable.txt에 저장한다. - 주 프로세스를
kill -9로 죽이고 systemd 가 되살리는지 본다./root/svc/kill.txt에old_pid=·new_pid=를 적는다. - 유닛에 샌드박싱(NoNewPrivileges=yes, ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, ReadWritePaths=/var/lib/orders)을 더한다. 바꾸기 전에
systemd-analyze security점수를 재 두고, 파일을 고친 직후 systemctl 이 내는 daemon-reload 경고를/root/svc/reload.txt에 저장한 뒤, daemon-reload·재시작하고 다시 잰다./root/svc/security.txt에before=·after=를 적는다. - 환경 파일의
ORDERS_TOKEN을 새 값으로 바꾸고 서비스에 반영한다./root/svc/rotate.txt에old_token_sha=·new_token_sha=(토큰의 sha256 16진수)와daemon_reload_needed=(yes 또는 no)를 적는다. - 인계용 점검 스크립트
/root/svc/verify.sh를 쓴다.bash verify.sh <유닛>이 그 유닛이 재부팅 뒤에도 뜰 조건(로드됨, enabled, WantedBy 에 multi-user.target, Restart 가 on-failure 또는 always, Wants 와 After 에 network-online.target, User 가 비어 있지 않고 root 가 아님, NeedDaemonReload=no)을 모두 갖추면 0, 하나라도 어기면 어긴 항목을 출력하고 0 이 아닌 값으로 끝난다. 채점기는 준비된 다른 유닛들(orders-audit·orders-report·orders-worker·orders-sync·orders-rootjob)에도 돌려 본다.
참고
- 포트 주인:
ss -ltnp 'sport = :8181'. 프로세스를 품은 유닛:systemctl status <pid>또는/proc/<pid>/cgroup. - 속성은 파일이 아니라 systemd 가 로드한 값으로 본다:
systemctl show orders.service -p User,Restart,Wants,After,NeedDaemonReload. - 실패를 여러 번 되풀이하면
start-limit-hit로 더 이상 시작하지 않는다.systemctl reset-failed orders.service뒤 다시 시작한다. - 흔한 실수 1:
After=network-online.target만 적기. 순서만 정할 뿐 그 target 을 끌어오지 않는다. - 흔한 실수 2:
systemctl start로 떠 있는 것을 보고 끝내기. 부팅 때 기동은 enable 이 만든 심링크가 정한다. - 흔한 실수 3: ProtectSystem=strict 만 켜기. 데이터 디렉터리까지 읽기 전용이 되어 앱이
data dir error로 죽는다.
8181 을 잡고 있는 떠돌이 프로세스
8181 을 듣는 프로세스의 pid·user·cgroup·unit 과 재부팅 뒤 생존 여부를 /root/svc/legacy.txt 에 적는다.
ss 의 -p 는 소켓을 가진 프로세스를 보여 줍니다. /proc//cgroup 의 경로는 이 프로세스가 어느 유닛 안에서 태어났는지 알려 줍니다. 그 유닛이 이 앱을 띄우는 책임을 진 유닛인지 생각해 보세요.
전용 계정과 환경 파일
시스템 계정 orders, /var/lib/orders(orders, 750), /etc/orders/orders.env(root:orders, 640)를 만든다.
useradd 의 --system 은 시스템 UID 대역을 쓰고, --shell 로 로그인 불가 셸을 줍니다. 환경 파일은 토큰이 들어 있으니 서비스 계정만 읽게 그룹과 권한을 맞추세요. 위키의 옛 토큰은 이미 새어 나간 값입니다.
첫 기동이 실패한 이유를 journal 에서
/etc/systemd/system/orders.service 를 쓰고 시작해 본 뒤, journal 의 실패 원인 줄을 /root/svc/why-failed.txt 에 옮긴다.
앱의 표준 출력은 journal 로 갑니다. journalctl -u 유닛 -o cat 은 앞머리 없이 메시지만 보여 줍니다. systemctl start 가 성공으로 끝나도 Type=simple 은 프로세스를 띄운 순간 성공이라 실제 결과는 status 와 journal 에서 봐야 합니다.
떠돌이를 내리고 서비스로 세우기
이전 담당자의 프로세스를 내리고 orders.service 를 active 로 만든다. healthz 가 orders 사용자와 주 프로세스 PID 로 응답해야 한다.
root 로 떠 있는 앱만 골라 내리세요(orders 계정의 프로세스를 같이 죽이면 안 됩니다). 앞 단계의 실패가 반복돼 시작 횟수 제한에 걸렸다면 reset-failed 가 필요합니다.
enable 이 만드는 심링크 하나
orders.service 를 enable 하고 출력(심링크 경로 포함)을 /root/svc/enable.txt 에 저장한다.
enable 은 서비스를 시작하지 않습니다. [Install] 의 WantedBy 를 읽어 그 target 의 .wants 디렉터리에 심링크를 만들 뿐이고, 부팅 때 그 target 이 이 서비스를 끌어옵니다. 메시지는 표준 오류로 나옵니다.
kill -9 해도 되살아나는가
주 프로세스를 kill -9 로 죽이고 되살아난 PID 와 함께 /root/svc/kill.txt 에 old_pid=·new_pid= 를 적는다.
주 프로세스 PID 는 systemctl show 의 MainPID 입니다. Restart=on-failure 가 어떤 종료를 실패로 치는지 매뉴얼의 표를 보세요. RestartSec 만큼 기다린 뒤 새 PID 가 생깁니다.
샌드박싱과 daemon-reload 누락
점수를 재고, 유닛에 샌드박싱을 더하고, daemon-reload 경고를 /root/svc/reload.txt 에 남긴 뒤 반영하고, /root/svc/security.txt 에 before=·after= 를 적는다.
systemd-analyze security 의 마지막 줄이 전체 노출 점수입니다(낮을수록 좁게 가둔 것). ProtectSystem=strict 는 파일 시스템 전체를 읽기 전용으로 만들므로 앱이 쓰는 경로를 따로 열어야 합니다. 파일을 고친 직후의 systemctl status 를 읽어 보세요.
토큰 교체는 무엇으로 반영되나
ORDERS_TOKEN 을 새 값으로 바꿔 서비스에 반영하고 /root/svc/rotate.txt 에 old_token_sha=·new_token_sha=·daemon_reload_needed= 를 적는다.
EnvironmentFile 은 유닛 설정이 아니라 프로세스를 띄우기 직전에 읽는 파일입니다. 유닛 파일을 고친 것과 무엇이 다른지, 그리고 이미 떠 있는 프로세스의 환경은 언제 바뀌는지 생각해 보세요. 해시는 줄바꿈 없이 토큰만 넣어 계산합니다.
재부팅 없이 부팅을 증명하는 점검 스크립트
유닛이 재부팅 뒤에도 뜰 조건을 모두 갖췄는지 판정하는 /root/svc/verify.sh 를 쓴다.
파일을 grep 하지 말고 systemctl show 와 is-enabled 로 systemd 가 로드한 값을 보세요. 없는 유닛도 show 는 성공하지만 LoadState 가 not-found 입니다. 한 항목에서 바로 끝내지 말고 어긴 항목을 모두 출력하면 인계받는 사람이 한 번에 고칩니다.