M0. YAML과 Linux 실행 토대
1. 왜 필요한가
섹션 제목: “1. 왜 필요한가”예시 3서비스 시스템의 web-app, api-service, ai-service를 NKS로 옮길 때 manifest를
읽는 것만으로는 부족합니다. YAML의 설정이 어떤 프로세스 환경과 listen port, 로그로
이어지는지 알아야 AI가 제안한 변경을 검토하고 장애의 첫 조사 지점을 고를 수 있습니다.
개념 정의의 정본은 Linux Basics와
Process & Thread입니다. 원본 경로는 각각
content/topics/L4/linux-basics.mdx, content/topics/L4/process-thread.mdx이며, 이
모듈은 정의를 반복하지 않고 실행과 관찰을 담당합니다.
2. 핵심 개념
섹션 제목: “2. 핵심 개념”| 입력·상태 | Linux에서의 실체 | 관찰 방법 | Kubernetes에서 다시 만나는 곳 |
|---|---|---|---|
| YAML mapping·sequence·scalar | 파서가 만드는 key-value·목록·단일 값의 트리 | Ruby YAML parser의 결과와 exit code | Pod spec, ConfigMap, Helm values |
| 환경변수 | 프로세스 시작 시 전달된 문자열 key-value | /proc/<PID>/environ | env, ConfigMap·Secret 주입 |
| 프로세스·PID | 실행 중인 프로그램 인스턴스와 식별자 | ps, /proc/<PID> | container의 main process |
| listen port | 프로세스가 요청을 받으려고 연 TCP socket | netstat, 실제 TCP 요청 | containerPort, Service target port |
| stdout·stderr | 정상 결과와 오류를 내보내는 표준 stream | redirection, docker logs | kubectl logs, log collector |
flowchart LR YAML["YAML: 원하는 설정"] --> Review["파싱·구조 검토"] Review --> Env["환경변수 주입"] Env --> Process["프로세스 시작 / PID"] Process --> Socket["port listen"] Process --> Streams["stdout / stderr"] Socket --> Request["요청 성공·실패"] Streams --> Logs["운영 로그"]
이 흐름에서 YAML parse 성공은 첫 관문일 뿐입니다. key가 오타여도 문법상 유효할 수 있고,
프로세스가 살아 있어도 다른 port를 듣거나 시작 오류를 stdout·stderr에 남길 수 있습니다.
따라서 운영 판단은 문법 → 주입 → 프로세스 → socket → 로그 순서로 증거를 좁힙니다.
시점 의존 기준은 2026-07-15에 확인했습니다.
- YAML 1.2.2 specification: mapping, sequence, scalar와 indentation
- Linux kernel procfs documentation: PID별
environ,status,fd - Docker run documentation:
-e환경변수 주입 - Docker logging documentation: stdout·stderr와
docker logs
3. 직관 비유
섹션 제목: “3. 직관 비유”| 개념 | 비유 | 비유의 경계 |
|---|---|---|
| YAML indentation | 괄호 대신 들여쓰기로 범위를 표시한 서류 양식 | 들여쓰기가 맞아도 key 의미까지 검증되지는 않습니다. |
| 프로세스·PID | 같은 조리법으로도 따로 운영되는 주방과 주방 번호 | 실제 프로세스는 메모리·파일·socket 같은 OS 상태도 가집니다. |
| 환경변수 | 주방을 열 때 전달하고 봉인한 당일 운영 카드 | 실행 중인 프로세스의 카드를 밖에서 바꿔 끼울 수는 없습니다. |
| port | 주방이 주문을 받기로 연 번호 창구 | port 번호를 문서에 적는 것과 실제 socket을 여는 것은 다릅니다. |
| stdout·stderr | 정상 안내 방송과 장애 안내 방송 | 운영에서는 두 stream이 같은 log backend로 합쳐질 수도 있습니다. |
4. 핸즈온 랩
섹션 제목: “4. 핸즈온 랩”검증 환경
섹션 제목: “검증 환경”| 항목 | 검증값 |
|---|---|
| 기준일 | 2026-07-15 |
| host | macOS arm64 |
| Docker client / engine | 29.6.1 / 29.6.1 |
| k3d | 5.9.0 |
| kubectl | 1.36.1 — Kubernetes server를 사용하지 않음 |
| Helm | 4.2.3 — 이 모듈에서는 사용하지 않음 |
| Linux image | ruby:3.4.7-alpine3.22 + digest sha256:2a9684…70b89 |
| evidence 환경 | local-container; Kubernetes context·namespace는 명시적 N/A |
모든 명령 묶음의 검증 등급은 executed-local입니다. image digest 전체 값은 Setup 명령에
고정되어 있습니다. macOS host에서 Docker Desktop의 Linux VM 안에 컨테이너를 실행하므로,
ps, /proc, netstat 관찰값은 Linux 컨테이너 기준입니다.
사전 조건
섹션 제목: “사전 조건”- 저장소 root에서 실행합니다.
- Docker Desktop daemon이 실행 중이어야 합니다.
node automation/k8s-workbook/preflight.mjs가 PASS여야 합니다.- 이 모듈은 Kubernetes API를 호출하지 않습니다. 현재 kubectl context나 NKS namespace를 읽거나 변경하지 않습니다.
- 예상 소요 시간은 75분입니다.
Setup
섹션 제목: “Setup”기존 fixture 컨테이너만 제거한 뒤, Ruby 프로세스가 PID 1로 실행되는 고정 Linux
컨테이너를 만듭니다. PORT와 APP_ENV는 프로세스 시작 시 주입합니다.
node automation/k8s-workbook/preflight.mjsdocker rm -f k8s-wb-m0 >/dev/null 2>&1 || truedocker run -d --name k8s-wb-m0 \ -e APP_ENV=workbook \ -e PORT=4567 \ ruby:3.4.7-alpine3.22@sha256:2a968472f07d03fd9c9fdfe220881c18fc0b28eb4d514b36b2c2d23810d70b89 \ ruby -rsocket -e '$stdout.sync = trueport = Integer(ENV.fetch("PORT"))server = TCPServer.new("0.0.0.0", port)puts "READY pid=#{Process.pid} env=#{ENV.fetch("APP_ENV")} port=#{port}"trap("TERM") { puts "STOP pid=#{Process.pid}"; exit }loop do client = server.accept request = client.gets.to_s path = request.split[1] puts "REQUEST path=#{path}" client.write "HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nok\n" client.closeend'for attempt in 1 2 3 4 5; do docker logs k8s-wb-m0 2>&1 | grep -q '^READY' && break sleep 1donedocker exec k8s-wb-m0 ruby --versiondocker logs k8s-wb-m0 2>&1예상 출력:
PASS platform: darwin/arm64PASS architecture: arm64PASS docker-cli: v29.6.1PASS docker-engine: v29.6.1PASS k3d: v5.9.0PASS kubectl-skew: v1.36.1PASS helm: v4.2.3<64자리 container ID>ruby 3.4.7 (...) +PRISM [aarch64-linux-musl]READY pid=1 env=workbook port=4567위 출력은 arm64 실측값입니다. container ID, Ruby build revision,
PASS platform·PASS architecture의 architecture 값, Ruby 출력 마지막의 architecture
suffix는 변동 필드입니다. x64 host가 preflight를 통과하면 이 architecture 관련 문자열이
달라도 정상이며, 검증하지 않은 x64 예상 문자열을 고정해 제시하지 않습니다. 핵심 불변값은
Ruby 3.4.7, pid=1, env=workbook, port=4567입니다. 5초 안에 READY가 없으면 다음
랩으로 가지 말고 docker logs k8s-wb-m0와 Docker daemon 상태를 확인합니다.
Lab 1 — YAML을 데이터 트리로 읽기
섹션 제목: “Lab 1 — YAML을 데이터 트리로 읽기”mapping 안에 scalar와 sequence를 둔 YAML을 만들고, parser가 실제로 만든 값을 읽습니다.
docker exec k8s-wb-m0 sh -c "printf '%s\n' \ 'service:' \ ' name: web-app' \ ' port: 3000' \ ' features:' \ ' - stdout' \ ' - env' > /tmp/service.yaml"docker exec k8s-wb-m0 ruby -ryaml -e 'doc = YAML.safe_load_file("/tmp/service.yaml")service = doc.fetch("service")puts "service=#{service.fetch("name")} port=#{service.fetch("port")} features=#{service.fetch("features").join(",")}"'예상 출력:
service=web-app port=3000 features=stdout,env이번에는 port의 indentation을 한 칸 더 넣어 의도적으로 parse를 실패시킵니다. 이 명령
묶음의 예상 exit code는 1입니다.
docker exec k8s-wb-m0 sh -c "printf '%s\n' \ 'service:' \ ' name: web-app' \ ' port: 3000' > /tmp/service.yaml"docker exec k8s-wb-m0 ruby -ryaml -e 'begin YAML.safe_load_file("/tmp/service.yaml")rescue Psych::SyntaxError => error warn "YAML_ERROR line=#{error.line}" exit 1end'예상 출력과 exit code:
YAML_ERROR line=3exit code: 1같은 파일의 indentation을 고치고 다시 parse합니다. 복구가 끝났다는 기준은 exit code 0과 구조화된 값 출력입니다.
docker exec k8s-wb-m0 sh -c "printf '%s\n' \ 'service:' \ ' name: web-app' \ ' port: 3000' > /tmp/service.yaml"docker exec k8s-wb-m0 ruby -ryaml -e 'service = YAML.safe_load_file("/tmp/service.yaml").fetch("service")puts "recovered=#{service.fetch("name")}:#{service.fetch("port")}"'예상 출력:
recovered=web-app:3000Lab 2 — stdout과 stderr를 분리해 보기
섹션 제목: “Lab 2 — stdout과 stderr를 분리해 보기”정상 결과와 오류 stream을 서로 다른 파일로 보낸 뒤 다시 읽습니다.
docker exec k8s-wb-m0 sh -c 'sh -c "printf normal-output; printf warning-output >&2" > /tmp/stdout 2> /tmp/stderrprintf "stdout="; cat /tmp/stdoutprintf "\nstderr="; cat /tmp/stderrprintf "\n"'예상 출력:
stdout=normal-outputstderr=warning-outputLab 3 — 프로세스, 환경변수, listen port를 한 줄로 연결하기
섹션 제목: “Lab 3 — 프로세스, 환경변수, listen port를 한 줄로 연결하기”PID 1의 프로세스 상태, 시작 환경, 실제 listen socket을 차례로 봅니다.
docker exec k8s-wb-m0 sh -c 'printf "process="ps -o pid,ppid,stat,comm | grep "^ *1 "printf "env="tr "\0" "\n" < /proc/1/environ | grep -E "^(APP_ENV|PORT)=" | sort | paste -sd, -printf "listen="netstat -lntp 2>/dev/null | grep ":4567 " | tr -s " " | cut -d " " -f4,7'예상 출력:
process= 1 0 S rubyenv=APP_ENV=workbook,PORT=4567listen=0.0.0.0:4567 1/rubySTAT 값은 실행 순간에 S 또는 R로 달라질 수 있는 변동 필드입니다. PID 1의 Ruby가
PORT=4567을 읽고 같은 port를 listen한다는 연결이 불변 관찰입니다.
긴 pipeline은 왼쪽부터 작은 변환을 연결한 것입니다. 막히면 한 번에 고치지 말고 해당 단계의 입력을 먼저 출력해 확인합니다.
| 단계 | 하는 일 | 실패하면 먼저 볼 것 |
|---|---|---|
ps ... | grep | 전체 프로세스 중 PID 1 행만 남김 | ps -o pid,ppid,stat,comm의 원본 행 |
tr | NUL로 구분된 /proc/1/environ을 한 줄당 한 변수로 변환 | /proc/1/environ 읽기 권한과 PID 존재 여부 |
grep -E | APP_ENV, PORT만 선택 | key 철자와 실제 주입 여부 |
sort | paste | 출력 순서를 고정하고 쉼표 한 줄로 합침 | paste 전 두 줄의 값 |
netstat ... | grep | 전체 listen socket에서 4567 port만 선택 | netstat -lntp 원본에 port가 있는지 |
tr -s | cut | 공백을 정규화하고 주소·PID/program만 표시 | 앞 단계의 column 간격 |
실제 TCP 요청을 보내고, 응답과 PID 1이 stdout에 남긴 요청 로그를 함께 봅니다.
docker exec k8s-wb-m0 ruby -rsocket -e 'socket = TCPSocket.new("127.0.0.1", 4567)socket.write("GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n")puts "response=#{socket.read.lines.last.strip}"socket.close'docker logs k8s-wb-m0 2>&1예상 출력:
response=okREADY pid=1 env=workbook port=4567REQUEST path=/healthdocker exec -e로 새 자식 프로세스의 환경을 바꿔도 이미 실행 중인 PID 1의 환경은 바뀌지
않는다는 점을 확인합니다.
docker exec -e APP_ENV=debug k8s-wb-m0 sh -c 'printf "child=%s\n" "$APP_ENV"printf "pid1="tr "\0" "\n" < /proc/1/environ | grep "^APP_ENV="'예상 출력:
child=debugpid1=APP_ENV=workbookCleanup
섹션 제목: “Cleanup”docker stop은 PID 1에 SIGTERM을 보내며, 앱은 종료 직전에 stdout으로 STOP을 남깁니다.
컨테이너를 제거한 뒤 같은 이름의 잔여 리소스가 0개인지 확인합니다.
docker stop --timeout 3 k8s-wb-m0docker logs k8s-wb-m0 2>&1 | tail -1docker rm k8s-wb-m0test -z "$(docker ps -a --filter name=^/k8s-wb-m0$ --format '{{.Names}}')" \ && echo 'cleanup=container-absent'예상 출력:
k8s-wb-m0STOP pid=1k8s-wb-m0cleanup=container-absent5. 관찰 포인트
섹션 제목: “5. 관찰 포인트”| 명령 이후 | 보게 되는 것 | 운영 판단 |
|---|---|---|
| 정상 YAML parse | nested mapping과 sequence가 의도한 값으로 읽힘 | 문법 검사는 통과했지만 Kubernetes schema 검사는 아직 아님 |
| indentation 오류 | line 3, exit code 1 | 배포 전 중단하고 YAML부터 복구 |
/proc/1/environ | PID 1이 시작할 때 받은 APP_ENV, PORT | 현재 shell 값이 아니라 실행 프로세스 값을 기준으로 판단 |
netstat | 1/ruby가 0.0.0.0:4567 listen | 프로세스 생존과 port 준비를 별도 증거로 확인 |
TCP 요청 + docker logs | 응답 ok와 REQUEST path=/health | socket 도달성과 앱 관측 신호를 연결 |
docker exec -e | 자식은 debug, PID 1은 계속 workbook | 실행 중 설정 변경 대신 workload 재생성이 필요 |
| Cleanup | STOP pid=1, container-absent | graceful stop 관찰 후 fixture 0개 확인 |
NKS에서도 조사 순서는 같습니다. Pod YAML의 env와 containerPort는 선언이고, 실제
프로세스 환경·listen socket·stdout은 런타임 증거입니다. 다만 이 모듈의 결과는 로컬 Linux
컨테이너 실측이며 NKS 실측으로 표현하지 않습니다.
6. 셀프체크 Q&A
섹션 제목: “6. 셀프체크 Q&A”Q1. YAML parser는 통과했는데 AI가 추가한 containerProt: 3000이 배포에서 거부됐습니다. 어디까지 확인된 상태인가요?
섹션 제목: “Q1. YAML parser는 통과했는데 AI가 추가한 containerProt: 3000이 배포에서 거부됐습니다. 어디까지 확인된 상태인가요?”YAML의 mapping 문법만 유효하다는 사실이 확인됐습니다. containerProt가 Kubernetes가 아는
field인지는 별도 schema 검증 문제입니다. 오타를 containerPort로 고치기 전에는 parser
성공을 배포 가능 판정으로 사용하면 안 됩니다.
Q2. api-service 컨테이너 프로세스는 살아 있지만 요청이 거부됩니다. 어떤 순서로 증거를 좁히시겠어요?
섹션 제목: “Q2. api-service 컨테이너 프로세스는 살아 있지만 요청이 거부됩니다. 어떤 순서로 증거를 좁히시겠어요?”PID와 로그를 확인하고, PID 1이 받은 PORT 환경변수와 실제 listen port를 비교한 뒤, 같은
network namespace 안에서 TCP 요청을 보냅니다. 프로세스 생존, socket listen, 요청 응답은
서로 다른 gate이므로 하나의 신호로 대체하지 않습니다.
Q3. AI가 실행 중인 컨테이너에 docker exec -e APP_ENV=production을 적용해 설정을 고치자고 제안했습니다. 승인해도 될까요?
섹션 제목: “Q3. AI가 실행 중인 컨테이너에 docker exec -e APP_ENV=production을 적용해 설정을 고치자고 제안했습니다. 승인해도 될까요?”승인하지 않습니다. -e는 새 docker exec 자식 프로세스에만 적용되고 이미 실행 중인 PID
1 환경은 바뀌지 않습니다. Kubernetes에서 ConfigMap·Secret 객체만 수정해도 env로 주입된
기존 프로세스나 Pod가 자동으로 재시작되지는 않습니다. Pod template 변경, checksum annotation
갱신 또는 명시적 restart처럼 새 Pod를 만드는 별도 rollout 동작을 사용하고, 새 프로세스
환경과 로그를 다시 확인해야 합니다.
7. 흔한 함정
섹션 제목: “7. 흔한 함정”함정 1. YAML parse 성공을 Kubernetes 유효성 검증으로 착각합니다
섹션 제목: “함정 1. YAML parse 성공을 Kubernetes 유효성 검증으로 착각합니다”YAML parser는 indentation과 자료형을 읽지만 Kubernetes resource kind별 field 의미까지
알지 못합니다. Kubernetes schema 검증은 M2 이후 kubectl과 API Server 경계에서 다룹니다.
함정 2. 프로세스가 보이면 port도 열렸다고 판단합니다
섹션 제목: “함정 2. 프로세스가 보이면 port도 열렸다고 판단합니다”프로세스는 설정 오류 뒤 대기 중일 수 있고, 예상과 다른 port를 들을 수도 있습니다. PID, 환경변수, listen socket, 실제 요청을 순서대로 분리해 확인합니다.
함정 3. 환경변수와 stdout에 실제 Secret을 출력합니다
섹션 제목: “함정 3. 환경변수와 stdout에 실제 Secret을 출력합니다”/proc/<PID>/environ과 로그는 운영자·수집 시스템에 노출될 수 있습니다. 이 랩은 실습용 fixture
값만 사용합니다. 실제 Secret 주입과 redaction은 M5에서 다루며, control plane 내부, CNI
구현, Terraform 작성은 이 워크북의 심화·제외 범위입니다.
8. 다음 모듈 연결 고리
섹션 제목: “8. 다음 모듈 연결 고리”M1에서는 방금 관찰한 프로세스·환경변수·port·stdout 계약을 Dockerfile, image, registry와 Compose로 패키징해 같은 실행 결과를 재현합니다.