콘텐츠로 이동

M0. YAML과 Linux 실행 토대

예시 3서비스 시스템의 web-app, api-service, ai-service를 NKS로 옮길 때 manifest를 읽는 것만으로는 부족합니다. YAML의 설정이 어떤 프로세스 환경과 listen port, 로그로 이어지는지 알아야 AI가 제안한 변경을 검토하고 장애의 첫 조사 지점을 고를 수 있습니다.

개념 정의의 정본은 Linux BasicsProcess & Thread입니다. 원본 경로는 각각 content/topics/L4/linux-basics.mdx, content/topics/L4/process-thread.mdx이며, 이 모듈은 정의를 반복하지 않고 실행과 관찰을 담당합니다.

입력·상태Linux에서의 실체관찰 방법Kubernetes에서 다시 만나는 곳
YAML mapping·sequence·scalar파서가 만드는 key-value·목록·단일 값의 트리Ruby YAML parser의 결과와 exit codePod spec, ConfigMap, Helm values
환경변수프로세스 시작 시 전달된 문자열 key-value/proc/<PID>/environenv, ConfigMap·Secret 주입
프로세스·PID실행 중인 프로그램 인스턴스와 식별자ps, /proc/<PID>container의 main process
listen port프로세스가 요청을 받으려고 연 TCP socketnetstat, 실제 TCP 요청containerPort, Service target port
stdout·stderr정상 결과와 오류를 내보내는 표준 streamredirection, docker logskubectl 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 indentation괄호 대신 들여쓰기로 범위를 표시한 서류 양식들여쓰기가 맞아도 key 의미까지 검증되지는 않습니다.
프로세스·PID같은 조리법으로도 따로 운영되는 주방과 주방 번호실제 프로세스는 메모리·파일·socket 같은 OS 상태도 가집니다.
환경변수주방을 열 때 전달하고 봉인한 당일 운영 카드실행 중인 프로세스의 카드를 밖에서 바꿔 끼울 수는 없습니다.
port주방이 주문을 받기로 연 번호 창구port 번호를 문서에 적는 것과 실제 socket을 여는 것은 다릅니다.
stdout·stderr정상 안내 방송과 장애 안내 방송운영에서는 두 stream이 같은 log backend로 합쳐질 수도 있습니다.
항목검증값
기준일2026-07-15
hostmacOS arm64
Docker client / engine29.6.1 / 29.6.1
k3d5.9.0
kubectl1.36.1 — Kubernetes server를 사용하지 않음
Helm4.2.3 — 이 모듈에서는 사용하지 않음
Linux imageruby: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분입니다.

기존 fixture 컨테이너만 제거한 뒤, Ruby 프로세스가 PID 1로 실행되는 고정 Linux 컨테이너를 만듭니다. PORTAPP_ENV는 프로세스 시작 시 주입합니다.

Terminal window
node automation/k8s-workbook/preflight.mjs
docker rm -f k8s-wb-m0 >/dev/null 2>&1 || true
docker 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 = true
port = 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.close
end
'
for attempt in 1 2 3 4 5; do
docker logs k8s-wb-m0 2>&1 | grep -q '^READY' && break
sleep 1
done
docker exec k8s-wb-m0 ruby --version
docker logs k8s-wb-m0 2>&1

예상 출력:

PASS platform: darwin/arm64
PASS architecture: arm64
PASS docker-cli: v29.6.1
PASS docker-engine: v29.6.1
PASS k3d: v5.9.0
PASS kubectl-skew: v1.36.1
PASS 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가 실제로 만든 값을 읽습니다.

Terminal window
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입니다.

Terminal window
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 1
end'

예상 출력과 exit code:

YAML_ERROR line=3
exit code: 1

같은 파일의 indentation을 고치고 다시 parse합니다. 복구가 끝났다는 기준은 exit code 0과 구조화된 값 출력입니다.

Terminal window
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:3000

Lab 2 — stdout과 stderr를 분리해 보기

섹션 제목: “Lab 2 — stdout과 stderr를 분리해 보기”

정상 결과와 오류 stream을 서로 다른 파일로 보낸 뒤 다시 읽습니다.

Terminal window
docker exec k8s-wb-m0 sh -c '
sh -c "printf normal-output; printf warning-output >&2" > /tmp/stdout 2> /tmp/stderr
printf "stdout="; cat /tmp/stdout
printf "\nstderr="; cat /tmp/stderr
printf "\n"'

예상 출력:

stdout=normal-output
stderr=warning-output

Lab 3 — 프로세스, 환경변수, listen port를 한 줄로 연결하기

섹션 제목: “Lab 3 — 프로세스, 환경변수, listen port를 한 줄로 연결하기”

PID 1의 프로세스 상태, 시작 환경, 실제 listen socket을 차례로 봅니다.

Terminal window
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 ruby
env=APP_ENV=workbook,PORT=4567
listen=0.0.0.0:4567 1/ruby

STAT 값은 실행 순간에 S 또는 R로 달라질 수 있는 변동 필드입니다. PID 1의 Ruby가 PORT=4567을 읽고 같은 port를 listen한다는 연결이 불변 관찰입니다.

긴 pipeline은 왼쪽부터 작은 변환을 연결한 것입니다. 막히면 한 번에 고치지 말고 해당 단계의 입력을 먼저 출력해 확인합니다.

단계하는 일실패하면 먼저 볼 것
ps ... | grep전체 프로세스 중 PID 1 행만 남김ps -o pid,ppid,stat,comm의 원본 행
trNUL로 구분된 /proc/1/environ을 한 줄당 한 변수로 변환/proc/1/environ 읽기 권한과 PID 존재 여부
grep -EAPP_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에 남긴 요청 로그를 함께 봅니다.

Terminal window
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=ok
READY pid=1 env=workbook port=4567
REQUEST path=/health

docker exec -e로 새 자식 프로세스의 환경을 바꿔도 이미 실행 중인 PID 1의 환경은 바뀌지 않는다는 점을 확인합니다.

Terminal window
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=debug
pid1=APP_ENV=workbook

docker stop은 PID 1에 SIGTERM을 보내며, 앱은 종료 직전에 stdout으로 STOP을 남깁니다. 컨테이너를 제거한 뒤 같은 이름의 잔여 리소스가 0개인지 확인합니다.

Terminal window
docker stop --timeout 3 k8s-wb-m0
docker logs k8s-wb-m0 2>&1 | tail -1
docker rm k8s-wb-m0
test -z "$(docker ps -a --filter name=^/k8s-wb-m0$ --format '{{.Names}}')" \
&& echo 'cleanup=container-absent'

예상 출력:

k8s-wb-m0
STOP pid=1
k8s-wb-m0
cleanup=container-absent
명령 이후보게 되는 것운영 판단
정상 YAML parsenested mapping과 sequence가 의도한 값으로 읽힘문법 검사는 통과했지만 Kubernetes schema 검사는 아직 아님
indentation 오류line 3, exit code 1배포 전 중단하고 YAML부터 복구
/proc/1/environPID 1이 시작할 때 받은 APP_ENV, PORT현재 shell 값이 아니라 실행 프로세스 값을 기준으로 판단
netstat1/ruby0.0.0.0:4567 listen프로세스 생존과 port 준비를 별도 증거로 확인
TCP 요청 + docker logs응답 okREQUEST path=/healthsocket 도달성과 앱 관측 신호를 연결
docker exec -e자식은 debug, PID 1은 계속 workbook실행 중 설정 변경 대신 workload 재생성이 필요
CleanupSTOP pid=1, container-absentgraceful stop 관찰 후 fixture 0개 확인

NKS에서도 조사 순서는 같습니다. Pod YAML의 envcontainerPort는 선언이고, 실제 프로세스 환경·listen socket·stdout은 런타임 증거입니다. 다만 이 모듈의 결과는 로컬 Linux 컨테이너 실측이며 NKS 실측으로 표현하지 않습니다.

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 동작을 사용하고, 새 프로세스 환경과 로그를 다시 확인해야 합니다.

함정 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 작성은 이 워크북의 심화·제외 범위입니다.

M1에서는 방금 관찰한 프로세스·환경변수·port·stdout 계약을 Dockerfile, image, registry와 Compose로 패키징해 같은 실행 결과를 재현합니다.