반복 적용과 책임 분리
100분 안팎
학습 목표
관측·판정·적용 코드를 분리하고 두 번째 실행에서 변경이 없는지 검사합니다.
개념
두 번째 적용이 조용해야 하는 이유
설정을 다시 실행했을 뿐인데 앱이 재기동되거나 파일 수정 시각이 바뀌면 점검과 변경 이력에 불필요한 잡음이 생깁니다. 멱등성은 원하는 상태를 적용한 뒤 다시 같은 작업을 해도 관리 대상의 결과가 같다는 성질입니다. 이번 실습은 결과만 같게 덮어쓰는 데서 한 단계 더 나아가 실제로 달라진 파일만 쓰고 두 번째 변경 수를 0으로 반환합니다. 파일의 내용과 권한을 따로 관측하여 내용이 같아도 권한이 어긋난 경우는 고칩니다.
관측·판정·적용의 경계를 정합니다
observe는 현재 디렉터리에서 파일 내용과 모드를 읽습니다. plan은 그 값을 명세와 비교하는 순수 함수입니다. apply는 검토한 계획이 아직 유효한지 다시 확인한 뒤 변경 파일을 씁니다. 이 세 가지를 하나의 거대한 셸 명령으로 합치면 어떤 조회가 실패했는지, 왜 썼는지 테스트하기 어렵습니다. 책임을 나누면 현재 상태를 읽는 코드와 쓰기 코드를 구별하고, 관측 대역으로 비교 정책을 먼저 검증할 수 있습니다.
이번 적용기가 소유하는 파일
허용 이름은 service.env와 health.conf 두 개입니다. 파일 경로를 문자열로 이어 붙이기 전에 이름이 허용 집합에 있는지 검사합니다. ../foreign은 OUT_OF_SCOPE로 거부합니다. 같은 이름의 심볼릭 링크가 있으면 SYMLINK_REFUSED로 중단합니다. 이것은 학습용 전용 디렉터리에서의 범위 검사이며 다른 사용자가 동시에 경로를 교체할 수 있는 공유 디렉터리의 경쟁 공격을 완전히 방어하는 구현은 아닙니다. 실습 경로의 쓰기 권한을 작업자에게만 줍니다.
모드와 내용의 비교 단위를 읽습니다
desired의 content는 최종 파일 문자열이고 mode는 0o600 또는 0o640 정수입니다. 0o 접두사는 Python 코드에서 8진수 표기입니다. JSON 파일로 저장하면 정수는 384 또는 416으로 표현될 수 있습니다. mode를 십진수 640으로 넣으면 권한 0640을 의미하지 않습니다. 이 구현은 허용 모드 외 값을 SPEC_INVALID로 거부합니다. stat.S_IMODE로 파일 종류 비트를 제외한 접근 모드를 읽고 같은 단위끼리 비교합니다.
없는 파일은 생성 계획이 될 수 있습니다
파일 관측기는 전용 대상 파일이 없을 때 content 빈 문자열과 mode 0을 반환합니다. 파일 존재 여부 조회에 성공한 부재이므로 생성 계획을 만들 수 있습니다. 앞 레슨의 JSON 관측 누락과는 다릅니다. PermissionError나 디렉터리 대상은 정상 부재로 바꾸지 않습니다. 경로의 부모가 준비되어 있고 작업자에게 생성 권한이 있는지 확인합니다. 디렉터리 대상이면 NOT_REGULAR로 중단하여 다른 종류의 자원을 덮어쓰지 않습니다.
검토 뒤 상태가 바뀌는 문제
사람이 두 파일의 계획을 읽는 동안 다른 담당자가 service.env를 고쳤을 수 있습니다. apply는 쓰기 직전에 observe와 plan을 다시 호출하여 새 changes가 approved와 동일한지 비교합니다. 다르면 STALE_PLAN으로 중단합니다. 새 상태를 자동으로 덮어쓰지 않고 새 계획을 리뷰합니다. 이 검사는 검토와 적용 사이의 변화 감지이며 마지막 관측 이후의 모든 동시 쓰기를 막는 잠금은 아닙니다. 이 실습은 한 작업자만 쓰는 경로를 전제로 합니다.
변경 행과 변경 파일 수는 다릅니다
service.env의 content와 mode가 모두 다르면 plan에는 두 행이 생깁니다. 그러나 쓰기 대상은 하나의 파일입니다. apply는 changes에서 resource 이름을 모아 중복을 제거하고 정렬합니다. 반환 값은 이 파일 수이므로 두 행을 두 번 적용했다고 세지 않습니다. 모드를 이미 맞춘 파일에 content만 달라져도 한 번 쓰고, 모든 속성이 맞으면 해당 파일을 열어 쓰지 않습니다. 변경 수의 단위를 문서에 적어 리뷰어가 결과를 해석하게 합니다.
같은 디렉터리에서 임시 파일을 준비합니다
replace_file은 대상의 부모에 .pending 이름의 임시 파일을 만듭니다. 내용을 모두 쓰고 flush와 fsync를 호출한 뒤 원하는 모드를 지정하고 os.replace로 대상 이름을 교체합니다. 읽는 쪽이 부분 문자열을 보는 위험을 줄이는 방식입니다. 임시 파일과 대상이 같은 파일시스템에 있도록 같은 부모를 사용합니다. 이 실습이 디렉터리 fsync나 전원 장애 이후 모든 복구까지 보장하는 트랜잭션은 아니라는 한계를 구분합니다.
후속 서비스 작업은 변경에 연결합니다
파일이 같아도 매번 systemctl start나 재기동을 호출하면 변경 수 0의 의미가 약해집니다. 로컬 적용기는 서비스 명령을 실행하지 않습니다. 미션에서는 유닛 파일이 실제로 달라진 경우에만 daemon-reload를 호출하며 그 작업이 앱 설정 로드 자체를 뜻하지는 않습니다. 실행 상태 검증은 별도 리허설 단계로 나눕니다. 유닛·환경 파일·방화벽 중 무엇이 달라졌는지에 따라 필요한 후속 작업이 다르기 때문입니다.
두 번째 0을 증명하는 검사
test_second_apply_and_metadata는 비어 있는 임시 폴더에 두 파일을 적용하여 변경 수 2를 확인합니다. 각 파일의 st_mtime_ns를 저장하고 같은 명세로 다시 적용합니다. 변경 수 0뿐 아니라 수정 시각도 유지되는지 검사합니다. foreign 파일의 내용도 보존되는지 확인합니다. 반환 숫자만 0으로 고쳐 통과시키려 하면 시각 비교에서 실패합니다. 결과 상태와 부작용을 함께 관측하는 테스트가 필요합니다.
권한 드리프트도 잡습니다
test_permission_drift는 내용 x가 같은 파일을 모드 0666으로 준비한 뒤 명세 0640을 적용합니다. 변경 수는 1이어야 합니다. 내용만 비교하면 허용 범위보다 넓은 쓰기 권한이 그대로 남습니다. 반대로 파일이 존재한다는 이유만으로 매번 chmod와 덮어쓰기를 반복하면 안정된 상태에서도 관리 작업이 발생합니다. 현재 내용과 모드를 모두 읽고 차이가 있는 자원에만 필요한 최종 상태를 적용합니다.
starter의 실패를 해석합니다
starter는 변경 목록 대신 desired의 모든 파일을 쓰도록 되어 있습니다. 첫 실행은 정상처럼 보이지만 두 번째 검사의 기대 0과 실제 2가 다릅니다. names를 changes에서 추출하도록 고칩니다. test_stale_plan이 실패하면 재관측 비교를 생략했는지 살펴봅니다. 테스트의 approved를 빈 배열로 바꾸어 맞추는 방식은 문제를 숨깁니다. 사람이 검토한 before·after가 적용 순간에도 유효해야 한다는 계약을 유지합니다.
오류 후 재실행을 판단합니다
쓰기 권한 오류가 발생하면 관리자 권한으로 바로 다시 실행하기보다 대상 경로와 기대 실행 계정을 확인합니다. 두 파일 중 하나만 적용되고 다음 파일에서 오류가 나면 전체 원자 적용이 완료된 것이 아닙니다. 현재 상태를 다시 관측하여 남은 차이를 검토합니다. 이번 apply는 파일별 교체이며 여러 파일의 일괄 복구 기능은 제공하지 않습니다. 서비스 설정 하나의 사전 사본과 실패 복구 경계는 다음 레슨에서 별도로 다룹니다.
반복 적용 결과를 설명합니다
완료 후에는 처음 바뀐 파일 수, 두 번째 0, 권한만 다른 사례, 검토 중 상태가 바뀐 사례를 재현할 수 있어야 합니다. 함수 분리와 테스트 기법의 더 넓은 예제는 Python 서재로 연결합니다. 여기서는 새 외부 패키지 없이 unittest와 tempfile을 사용합니다. 스크립트를 반복해 실행했다는 사실이 아니라 동일 상태에 불필요한 쓰기가 없었다는 증거를 변경 기록에 남깁니다.
파일 교체의 동일 파일시스템 조건과 성공 시 원자성은 Python os.replace 공식 문서를 참고합니다.
따라하기
8진 모드의 JSON 정수 확인
JSON에는 8진 접두사를 쓰지 않습니다. 이 숫자는 파일 개수와 무관합니다.
import json
print(json.dumps({"mode":0o640}))
print(oct(416))실행 결과
{"mode": 416}
0o640
첫 적용과 두 번째 적용 비교
solution 예제는 새 임시 경로에서 파일 한 개를 적용한 뒤 같은 명세를 적용합니다.
python3 -B demo.py apply실행 결과
FIRST 1 SECOND 0
쓰기 대상 선택 완성
starter apply.py의 names를 changes의 resource 집합에서 계산합니다. 네 테스트에서 수정 시각·모드·범위·검토 중 변화가 확인되어야 합니다.
bash check.sh확인 문제
실습
apply.py에서 desired 전체 대신 실제 changes의 자원만 씁니다. 첫 적용 2, 두 번째 0과 수정 시각 보존, 권한 드리프트, STALE_PLAN, 경로 이탈·링크 거부 검사를 통과합니다. 반환 숫자만 고치지 말고 쓰기 부작용을 제거합니다.
실행 명령
bash check.sh
기대 결과
4 tests, OK, 종료 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 점검 스크립트가 실패를 알리는 방식을 설명합니다.