Capstone Hands-on Lab B / Chapter 1-5 통합

Mission: Self-Heal, 무인 운영실을 세워라

Claude Code Deep Dive Workshop, Capstone B - 자율 복구 SRE 에이전트 (135분 목표 / 150분 상한)

이번 미션의 정체성은 "지키는 Claude"입니다. 작은 대상 서비스를 배포하고, 스스로 장애를 주입한 뒤, EC2 위 Claude Code 헤드리스가 혼자 진단하고 혼자 고치는 메딕 에이전트를 세웁니다. 피날레는 블라인드 카오스: 무슨 장애인지 사람은 모르고, 에이전트가 원인을 규명해 복구하고 MTTR을 보고합니다. 자율에는 반드시 가드레일이 따릅니다. Hook 인터록이 파괴적 명령을 차단하는 것까지가 오늘의 완성입니다.

환경 Claude Code on EC2 + Admin 계정 리전 ap-northeast-2 핵심 Headless 메딕 + 안전 인터록 Update 2026.07
MISSION BRIEFING

대상 서비스, 메딕, 그리고 인터록

시스템은 세 축입니다. 대상 서비스는 CDK로 배포하는 작은 주문 API(HTTP API + Lambda + DynamoDB). 카오스는 3가지 장애를 주입하는 스크립트. 메딕은 EC2에서 도는 셸 + Claude 헤드리스로, 런북을 읽고 감별 진단해 aws CLI로 직접 치료합니다. 그리고 메딕의 손은 PreToolUse 인터록이 붙잡습니다, 파괴적 명령은 exit 2로 차단됩니다.

아키텍처
                    ┌── chaos.sh (1 CONFIG_POISON / 2 THROTTLE / 3 ENV_BREAK / blind)
                    v
브라우저/curl ── HTTP API ── Lambda shop ── DynamoDB (flags + orders)
                    ^                          [대상 서비스, PatientStack]
                    │ /health 순찰, 처방 후 검증
[EC2] medic.sh 루프 ─┤
  └ claude -p (헤드리스, RUNBOOK 감별 진단)
      ├ --allowed-tools: aws logs / dynamodb / lambda / curl / Read
      ├ PreToolUse 인터록: 파괴 명령 exit 2 차단
      └ 결과 → INCIDENTS.md (판정 / ROOT_CAUSE / MTTR / 비용)
규칙 1 - 코드는 Claude에게 시킨다, 단 메딕의 손발은 도구 제한과 인터록으로 묶는다 규칙 2 - 각 미션은 Definition of Done으로만 판정한다 규칙 3 - 막히면 "막힐 때 열어보기" 완성본으로 복구하고 전진한다

캡스톤 A(Ship It)를 먼저 했다면 M0의 상당 부분은 확인만으로 통과합니다. 90분 시점에 M3를 못 끝냈다면 부록 컷라인을 적용하세요.

MISSION 00

발사 준비, 계획 승인까지

15분

환경 점검 후 superpowers로 4-Phase 계획을 승인합니다. 캡스톤 A를 마친 계정이라면 bootstrap과 플러그인은 이미 준비된 상태입니다.

1점검과 스캐폴드
Terminal, 전체 복사
aws sts get-caller-identity --query Account --output text
node -v && jq --version && npx cdk --version

mkdir -p ~/capstone/self-heal && cd ~/capstone/self-heal
npx cdk init app --language typescript
(npx cdk bootstrap > /tmp/bootstrap.log 2>&1 &)   # 이미 부트스트랩된 계정은 수 초 내 통과
git add -A && git commit -m "chore: cdk scaffold" 2>/dev/null || true
2superpowers 확인 또는 설치
Claude 세션 입력
/plugin

목록에 superpowers가 없으면 /plugin install superpowers@claude-plugins-official로 설치하고 세션을 재시작하세요 (마켓플레이스 미등록 시 /plugin marketplace add anthropics/claude-plugins-official 선행).

3브레인스토밍과 계획
Claude 세션 입력, 전체 복사
/superpowers:brainstorming 다음 요구사항으로 "Self-Healing 운영실"을 설계하자.
1) 대상 서비스: CDK TypeScript 단일 스택(PatientStack), HTTP API + Lambda(Node 22) + DynamoDB 단일 테이블(pk/sk)
   - GET /health: flags 항목(pk=flags, sk=flags)의 force_error가 true이거나 테이블 접근 실패면 500, 정상이면 200 {"ok":true}
   - GET /order: 정상일 때 주문 항목을 기록하고 order_id 반환
   - Outputs: ApiUrl, TableName, ShopFnName. 리전 ap-northeast-2, 리소스는 DESTROY
2) 운영 도구는 별도 셸 스크립트: chaos.sh(장애 주입 3종), medic.sh(헤드리스 진단과 복구), RUNBOOK.md(감별 진단표)
3) 안전장치: PreToolUse 인터록 훅으로 파괴적 aws 명령 차단, 메딕은 --allowed-tools로 손발 제한
4) 구현 순서 Phase 1(대상 서비스 스택) → Phase 2(런북+카오스) → Phase 3(메딕) → Phase 4(인터록+데몬+드릴 스킬)
5) 시간 제약 2시간: 단위 테스트 생략, 각 Phase는 smoke curl과 드릴 1회로 검증을 갈음한다
결정이 필요하면 이 범위 안에서 최소한으로만 물어봐.
자동화 옵션, auto mode 세션에 auto mode on을 선언해 두면 superpowers가 brainstorming → write-plan → execute-plan의 단계 전환과 승인 대기를 자동으로 이어 갑니다. 시간이 빠듯할 때 유용합니다. 단계마다 직접 검토하려면 기본(수동)을 유지하고, 되돌리려면 auto mode off입니다.

치트시트: 인증 없음(데모), 알림 채널 불필요(INCIDENTS.md 파일로 충분), 데몬은 nohup 백그라운드면 충분(systemd는 참고). 브레인스토밍 후 /superpowers:write-plan으로 계획을 만들고 Phase 4개 구성을 확인해 승인하세요.

worktree 순간이동, 다시 한 번 superpowers는 계획 후 git worktree로 이동합니다. 이후 deploy와 모든 운영 스크립트는 pwd로 확인한 그 경로에서 실행하세요.
DEFINITION OF DONE
MISSION 01

대상 서비스 배포, 정상 상태의 기준선

25분

Phase 1을 실행해 대상 서비스를 배포하고, 건강할 때의 모습(health 200, order 성공, 로그 흐름)을 눈에 익힙니다. 진단은 기준선을 아는 데서 시작합니다.

1Phase 1 실행과 배포
Claude 세션 입력
/superpowers:execute-plan 계획의 Phase 1(대상 서비스 스택)만 구현하고 멈춰줘. 구현 후 npx cdk synth --quiet 검증까지 하고 보고해.
Terminal, worktree에서
npx cdk deploy --require-approval never --outputs-file cdk-outputs.json
2기준선 확인
Terminal, 전체 복사
API=$(jq -r '.PatientStack.ApiUrl' cdk-outputs.json)
FN=$(jq -r '.PatientStack.ShopFnName' cdk-outputs.json)
export OPS_TARGET_API="$API" OPS_OUTPUTS="$PWD/cdk-outputs.json"

curl -s "$API/health" | jq .
curl -s "$API/order"  | jq .
aws logs tail "/aws/lambda/$FN" --since 5m | tail -5
출력 예시
{ "ok": true }
{ "ok": true, "order_id": "1753221049-a1b2c3" }
2026-07-22T22:30:11 START RequestId: ...
2026-07-22T22:30:11 END RequestId: ...
막힐 때 열어보기, 완성본 (대상 서비스 스택 3파일)
lib/patient-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2";
import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";

export class PatientStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const table = new dynamodb.Table(this, "ShopTable", {
      partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
      sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    const shopFn = new lambda.Function(this, "ShopFn", {
      runtime: lambda.Runtime.NODEJS_22_X,
      handler: "shop.handler",
      code: lambda.Code.fromAsset("lambda"),
      environment: { TABLE_NAME: table.tableName },
      timeout: cdk.Duration.seconds(10),
    });
    table.grantReadWriteData(shopFn);

    const api = new apigwv2.HttpApi(this, "PatientApi");
    const integ = new integrations.HttpLambdaIntegration("ShopInt", shopFn);
    api.addRoutes({ path: "/health", methods: [apigwv2.HttpMethod.GET], integration: integ });
    api.addRoutes({ path: "/order", methods: [apigwv2.HttpMethod.GET], integration: integ });

    new cdk.CfnOutput(this, "ApiUrl", { value: api.apiEndpoint });
    new cdk.CfnOutput(this, "TableName", { value: table.tableName });
    new cdk.CfnOutput(this, "ShopFnName", { value: shopFn.functionName });
  }
}
bin/self-heal.ts (cdk init 산출물 교체)
import * as cdk from "aws-cdk-lib";
import { PatientStack } from "./patient-stack";

const app = new cdk.App();
new PatientStack(app, "PatientStack", {
  env: { region: process.env.CDK_DEFAULT_REGION ?? "ap-northeast-2" },
  tags: { project: "capstone-self-heal" },
});
lambda/shop.mjs
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb";

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const TABLE = process.env.TABLE_NAME;

async function readFlags() {
  const res = await ddb.send(new GetCommand({
    TableName: TABLE,
    Key: { pk: "flags", sk: "flags" },
  }));
  return res.Item ?? {};
}

export const handler = async (event) => {
  const path = event.rawPath ?? "/";
  try {
    const flags = await readFlags();

    if (path.endsWith("/health")) {
      if (flags.force_error) {
        return { statusCode: 500, body: JSON.stringify({ ok: false }) };
      }
      return { statusCode: 200, body: JSON.stringify({ ok: true }) };
    }

    if (path.endsWith("/order")) {
      if (flags.force_error) {
        return { statusCode: 500, body: JSON.stringify({ ok: false, error: "order rejected" }) };
      }
      const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
      await ddb.send(new PutCommand({
        TableName: TABLE,
        Item: { pk: "order", sk: id, at: new Date().toISOString() },
      }));
      return { statusCode: 200, body: JSON.stringify({ ok: true, order_id: id }) };
    }

    return { statusCode: 404, body: JSON.stringify({ ok: false, error: "not found" }) };
  } catch (err) {
    console.error("SHOP_ERROR", err.name, err.message);
    return { statusCode: 500, body: JSON.stringify({ ok: false }) };
  }
};
DEFINITION OF DONE
MISSION 02

런북과 카오스, 병을 정의하다

20분

서브에이전트가 런북을 쓰는 동안(Chapter 2), 여러분은 카오스 스크립트를 배치하고 장애 1종을 수동으로 주입과 처방해 봅니다. 메딕에게 시키기 전에 사람이 먼저 한 번 고쳐 봐야 합니다.

1런북 병렬 작성 지시
Claude 세션 입력, 전체 복사
서브에이전트를 사용해 RUNBOOK.md를 작성해줘. 요구사항:
- 대상은 PatientStack이고 자원 이름은 cdk-outputs.json의 TableName, ShopFnName을 쓴다고 명시
- "감별 진단표" 표: 3가지 장애(ENV_BREAK, CONFIG_POISON, THROTTLE)의 증상 / 확인 명령 / 판정 / 원인 코드
  핵심 신호: ENV_BREAK는 로그의 ResourceNotFoundException, CONFIG_POISON은 flags.force_error=true,
  THROTTLE은 5xx인데 새 로그가 전혀 없고 get-function-concurrency가 0
- "처방" 절: 각 원인의 복구 aws 명령 한 줄씩 (ENV_BREAK 복구값은 cdk-outputs.json의 TableName)
- "검증" 절: /health 200 재확인, ENV 복구는 반영에 수 초 걸림 명시
2카오스 스크립트 배치 (런북이 써지는 동안)
Terminal, 전체 복사 (worktree에서)
cat > chaos.sh << 'CHAOSEOF'
#!/bin/bash
# 사용: ./chaos.sh 1|2|3|blind   (1 CONFIG_POISON, 2 THROTTLE, 3 ENV_BREAK)
set -euo pipefail
OUTS="${OPS_OUTPUTS:-cdk-outputs.json}"
TABLE=$(jq -r '.PatientStack.TableName' "$OUTS")
FN=$(jq -r '.PatientStack.ShopFnName' "$OUTS")

MODE="${1:?Usage: ./chaos.sh 1|2|3|blind}"
if [ "$MODE" = "blind" ]; then
  MODE=$(( (RANDOM % 3) + 1 ))
  echo "[chaos] 블라인드 모드, 무엇이 주입됐는지는 메딕만 알게 됩니다"
fi

case "$MODE" in
  1) aws dynamodb update-item --table-name "$TABLE" \
       --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' \
       --update-expression 'SET force_error = :t' \
       --expression-attribute-values '{":t":{"BOOL":true}}' ;;
  2) aws lambda put-function-concurrency --function-name "$FN" \
       --reserved-concurrent-executions 0 ;;
  3) aws lambda update-function-configuration --function-name "$FN" \
       --environment 'Variables={TABLE_NAME=broken-table-404}' ;;
  *) echo "unknown mode: $MODE"; exit 1 ;;
esac
echo "[chaos] 주입 완료. 이제 메딕이 일하게 두세요."
CHAOSEOF
chmod +x chaos.sh
3수동 드릴, 사람이 먼저 고쳐 본다
Terminal, 전체 복사
./chaos.sh 1
curl -s -o /dev/null -w 'health: %{http_code}\n' "$API/health"   # 500이어야 정상적인 비정상

TABLE=$(jq -r '.PatientStack.TableName' cdk-outputs.json)
aws dynamodb update-item --table-name "$TABLE" \
  --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' \
  --update-expression 'SET force_error = :f' \
  --expression-attribute-values '{":f":{"BOOL":false}}'

curl -s -o /dev/null -w 'health: %{http_code}\n' "$API/health"   # 다시 200
막힐 때 열어보기, 완성본 (RUNBOOK.md)
RUNBOOK.md
# Patient Service Runbook

대상: PatientStack (HTTP API + shop Lambda + DynamoDB)
자원 이름은 cdk-outputs.json의 TableName, ShopFnName을 사용한다.

## 감별 진단표

| 증상 | 확인 명령 | 판정 | 원인 코드 |
|---|---|---|---|
| /health 500, 로그에 SHOP_ERROR ResourceNotFoundException | aws logs tail /aws/lambda/<ShopFnName> --since 10m | Lambda 환경변수 TABLE_NAME 오염 | ENV_BREAK |
| /health 500, 로그는 정상 흐름, flags.force_error가 true | aws dynamodb get-item --table-name <TableName> --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' | 설정 플래그 오염 | CONFIG_POISON |
| /health 5xx인데 새 로그가 전혀 안 생김 | aws lambda get-function-concurrency --function-name <ShopFnName> 이 0 | 예약 동시성 0으로 질식 | THROTTLE |

## 처방

- CONFIG_POISON: aws dynamodb update-item --table-name <TableName> --key '{"pk":{"S":"flags"},"sk":{"S":"flags"}}' --update-expression 'SET force_error = :f' --expression-attribute-values '{":f":{"BOOL":false}}'
- THROTTLE: aws lambda delete-function-concurrency --function-name <ShopFnName>
- ENV_BREAK: aws lambda update-function-configuration --function-name <ShopFnName> --environment 'Variables={TABLE_NAME=<TableName>}' (올바른 값은 cdk-outputs.json에서 읽는다)

## 검증

처방 후 curl -s <ApiUrl>/health 가 200 {"ok":true} 를 반환할 때까지 확인한다.
ENV_BREAK 처방은 함수 설정 반영에 수 초가 걸릴 수 있다.
DEFINITION OF DONE
MISSION 03

메딕 에이전트, 진단과 치료의 자동화

30분

Chapter 5의 헤드리스 문법이 전부 들어간 medic.sh를 배치하고, 장애를 주입한 뒤 메딕 혼자 고치는 것을 지켜봅니다.

1메딕 배치
Terminal, 전체 복사 (worktree에서)
cat > medic.sh << 'MEDICEOF'
#!/bin/bash
# medic.sh - 대상 서비스 점검, 비정상이면 Claude 헤드리스로 진단과 복구, 결과를 INCIDENTS.md에 기록
set -uo pipefail
API="${OPS_TARGET_API:?OPS_TARGET_API 필요 (대상 서비스 ApiUrl)}"
OUTS="${OPS_OUTPUTS:-cdk-outputs.json}"
MAX_WAIT="${MEDIC_MAX_WAIT:-60}"

code=$(curl -s -o /tmp/health.json -w '%{http_code}' -m 5 "$API/health" || echo 000)
if [ "$code" = "200" ]; then
  echo "[medic] healthy"
  exit 0
fi

T0=$(date +%s)
echo "[medic] UNHEALTHY (http $code), Claude 진단 시작"

REPORT=$(claude -p "$(cat <<PROMPT
당신은 SRE 메딕 에이전트입니다. 대상 서비스 API가 비정상입니다 (HTTP $code).

런북:
$(cat RUNBOOK.md)

스택 출력(cdk-outputs.json):
$(cat "$OUTS")

마지막 헬스 응답: $(cat /tmp/health.json 2>/dev/null)

런북의 감별 진단표 순서로 원인을 특정하고, 런북의 처방 명령으로 직접 복구한 뒤
curl로 /health 200을 확인하세요. 보고서 마지막 줄은 정확히 다음 형식 한 줄로 끝내세요:
ROOT_CAUSE: <CONFIG_POISON|THROTTLE|ENV_BREAK>
PROMPT
)" --output-format json --max-turns 15 --max-budget-usd 1.00 \
  --allowed-tools "Bash(aws logs *)" "Bash(aws dynamodb *)" "Bash(aws lambda *)" "Bash(curl *)" "Read")

echo "$REPORT" | jq -r '.result' > /tmp/medic_report.md
COST=$(echo "$REPORT" | jq -r '.total_cost_usd // 0')

MTTR=""
i=0
while [ "$i" -lt "$((MAX_WAIT / 2))" ]; do
  c2=$(curl -s -o /dev/null -w '%{http_code}' -m 5 "$API/health" || echo 000)
  if [ "$c2" = "200" ]; then
    MTTR=$(( $(date +%s) - T0 ))
    break
  fi
  sleep 2
  i=$((i + 1))
done

TS=$(date '+%F %T')
if [ -n "$MTTR" ]; then
  CAUSE=$(grep -o 'ROOT_CAUSE: [A-Z_]*' /tmp/medic_report.md | tail -1)
  {
    echo "## $TS RECOVERED, MTTR ${MTTR}s, 비용 \$$COST"
    echo "$CAUSE"
    echo
    cat /tmp/medic_report.md
    echo
  } >> INCIDENTS.md
  echo "[medic] RECOVERED in ${MTTR}s / ${CAUSE:-원인 미표기}"
  exit 0
else
  {
    echo "## $TS FAILED, ${MAX_WAIT}s 내 미회복, 비용 \$$COST"
    cat /tmp/medic_report.md
    echo
  } >> INCIDENTS.md
  echo "[medic] 복구 실패, 사람 호출이 필요합니다"
  exit 1
fi
MEDICEOF
chmod +x medic.sh
bash -n medic.sh && echo "문법 OK"
메딕의 3중 잠금과 손발 제한 (Ch5 + Ch3) --max-turns 15--max-budget-usd 1.00이 폭주와 지출을 막고, --allowed-tools는 공백 구분 다중 인자로 aws logs / dynamodb / lambda / curl / Read만 허용합니다. 이 계정은 Admin이므로 실무 전환 시에는 메딕 전용 IAM 정책(로그 읽기 + 해당 테이블 UpdateItem + 해당 함수 설정 변경)으로 이중 방어하는 것이 Chapter 3의 정석입니다. Claude Code가 Bedrock 라우팅이면 ANTHROPIC_MODEL='global.anthropic.claude-sonnet-4-6' 한 줄로 메딕의 두뇌를 지정합니다.
21차 드릴, 지켜보기만 하세요
Terminal, 전체 복사
./chaos.sh 1
./medic.sh
tail -8 INCIDENTS.md
출력 예시
[chaos] 주입 완료. 이제 메딕이 일하게 두세요.
[medic] UNHEALTHY (http 500), Claude 진단 시작
[medic] RECOVERED in 41s / ROOT_CAUSE: CONFIG_POISON
## 2026-07-22 22:41:05 RECOVERED, MTTR 41s, 비용 $0.18
ROOT_CAUSE: CONFIG_POISON
1) 헬스 500 확인, 로그는 정상 흐름 → 감별표 2행
2) flags.force_error=true 확인, 처방 적용
3) /health 200 재확인

이 장면이 핵심입니다. 여러분은 명령 두 줄만 쳤고, 원인 규명과 aws 처방은 전부 에이전트가 했습니다. THROTTLE(2번)로 한 번 더 돌려 감별이 달라지는지 확인해 보세요. 새 로그가 안 생기는 장애는 로그를 읽어도 안 보입니다, 그래서 런북이 get-function-concurrency를 가르쳐 둔 것입니다.

DEFINITION OF DONE
MISSION 04

안전 인터록과 무인 순찰

20분

자율 에이전트에게 가장 먼저 줄 것은 자유가 아니라 브레이크입니다. PreToolUse 인터록(Chapter 4)으로 파괴적 명령을 차단하고, 메딕을 30초 주기 무인 데몬으로 올립니다.

1인터록 배치와 등록
Terminal, 전체 복사 (worktree에서)
mkdir -p .claude/hooks
cat > .claude/hooks/ops-interlock.sh << 'LOCKEOF'
#!/bin/bash
# PreToolUse(Bash) 인터록: 메딕이 절대 쓰면 안 되는 파괴적 명령 차단
INPUT=$(cat)
if command -v jq >/dev/null 2>&1; then
  CMD=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
else
  CMD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
fi

BLOCK='aws (iam |s3 rb|s3 rm|dynamodb delete-table|cloudformation delete-stack|ec2 terminate|lambda delete-function )'
if echo "$CMD" | grep -qE "$BLOCK"; then
  echo "[interlock] 차단: 파괴적 명령은 메딕 권한 밖입니다: $CMD" >&2
  exit 2
fi
exit 0
LOCKEOF
chmod +x .claude/hooks/ops-interlock.sh
Claude 세션 입력
.claude/settings.json에 PreToolUse 훅을 추가해줘. matcher는 "Bash", command는 "$CLAUDE_PROJECT_DIR/.claude/hooks/ops-interlock.sh" 하나야. 기존 설정은 유지해.
2인터록 라이브 테스트, 차단을 눈으로
Terminal, 훅 단위 테스트
echo '{"tool_input":{"command":"aws iam delete-role --role-name demo"}}' \
  | .claude/hooks/ops-interlock.sh; echo "exit: $?"
echo '{"tool_input":{"command":"aws lambda delete-function-concurrency --function-name x"}}' \
  | .claude/hooks/ops-interlock.sh; echo "exit: $?"
출력 예시
[interlock] 차단: 파괴적 명령은 메딕 권한 밖입니다: aws iam delete-role --role-name demo
exit: 2
exit: 0

치료 명령인 delete-function-concurrency는 통과하고, delete-function 은 막히는 것이 포인트입니다. 새 Claude 세션에서 "aws iam delete-role을 실행해봐"라고 시켜 보면 훅이 exit 2로 도구 호출 자체를 거부하는 것을 볼 수 있습니다.

3무인 순찰 데몬 기동
Terminal, 전체 복사
cat > medic-daemon.sh << 'DAEMONEOF'
#!/bin/bash
# 30초 주기 무인 순찰. 종료: kill %1 또는 pkill -f medic-daemon
while true; do
  ./medic.sh
  sleep "${MEDIC_INTERVAL:-30}"
done
DAEMONEOF
chmod +x medic-daemon.sh
nohup ./medic-daemon.sh > medic.log 2>&1 &
sleep 2 && tail -2 medic.log
상시 운영이라면 systemd (참고) 랩에서는 nohup으로 충분합니다. 상시 운영은 systemd 서비스 + 타이머로 승격하고, Environment=OPS_TARGET_API/OPS_OUTPUTS를 유닛에 명시하며, 로그는 Chapter 3의 OTel 파이프라인에 합류시키는 것이 정석입니다. 데몬 종료는 pkill -f medic-daemon 입니다.
DEFINITION OF DONE
MISSION 05

블라인드 카오스, 진짜 시험

20분

이제 무슨 장애가 들어갈지 여러분도 모릅니다. /drill 스킬로 블라인드 장애를 주입하고, 데몬이 규명한 원인과 MTTR을 확인합니다. 세 가지 장애를 전부 잡을 때까지 이어 가는 3연속 블라인드 챌린지가 오늘의 피날레입니다.

1/drill 스킬 설치
Terminal, 전체 복사 (worktree에서)
mkdir -p .claude/skills/drill
cat > .claude/skills/drill/SKILL.md << 'DRILLEOF'
---
description: 카오스 드릴 실행. 장애를 주입하고 메딕의 진단과 복구를 관전하며 MTTR을 보고한다.
disable-model-invocation: true
argument-hint: "[blind|1|2|3]"
allowed-tools: Bash(./chaos.sh *) Bash(./medic.sh) Bash(curl *) Bash(tail *) Bash(sleep *) Read
---

카오스 드릴을 집도하세요. 모드: $ARGUMENTS (기본 blind)

1. `./chaos.sh $ARGUMENTS` 로 장애를 주입하세요.
2. `curl -s -o /dev/null -w '%{http_code}' $OPS_TARGET_API/health` 로 대상 서비스가 실제로 쓰러졌는지 확인하세요.
3. 데몬이 돌고 있지 않다면 `./medic.sh` 를 직접 1회 실행하세요.
4. INCIDENTS.md의 마지막 엔트리를 읽고 다음을 표로 보고하세요: 판정(RECOVERED/FAILED), ROOT_CAUSE, MTTR, 비용.
5. 마지막으로 /health 가 200인지 재확인하고 "드릴 종료"를 선언하세요.

주의: 직접 aws 명령으로 고치지 마세요. 치료는 메딕의 일입니다. 당신은 드릴 집도의입니다.
DRILLEOF
echo "새 Claude 세션에서 /drill 사용 가능"
2블라인드 드릴 집도
Claude 세션 입력 (새 세션에서)
/drill blind
기대 결과 (에이전트 보고)
| 판정 | ROOT_CAUSE | MTTR | 비용 |
| RECOVERED | THROTTLE | 52s | $0.14 |
드릴 종료. /health 200 재확인 완료.
피날레, 3연속 블라인드 챌린지 회복 확인 후 /drill blind를 연달아 실행해, INCIDENTS.md에 세 가지 원인(CONFIG_POISON, THROTTLE, ENV_BREAK)이 모두 잡힐 때까지 도전하세요. blind는 랜덤이라 같은 장애가 반복될 수 있습니다, 그것도 실전입니다. 같은 블라인드 장애라도 감별 경로가 달라 MTTR이 갈립니다, 그 차이가 곧 런북 품질입니다.
3정리
Terminal, 전체 복사
grep -c "RECOVERED" INCIDENTS.md | xargs echo "오늘 자동 복구 횟수:"
grep -o 'ROOT_CAUSE: [A-Z_]*' INCIDENTS.md | sort | uniq -c   # 원인별 격파 현황
git add -A && git commit -m "feat: self-healing ops capstone"
pkill -f medic-daemon        # 데몬 종료

# 실습을 마쳤다면 비용 정리 (이어서 볼 계획이면 유지):
# npx cdk destroy --force
DEFINITION OF DONE
OPTION

하네스 엔지니어링, 작품을 성숙시키고 등급을 받다

+15분 옵션

지금까지 만든 것은 결과물입니다. 이 옵션은 그 결과물을 만든 작업 환경, 즉 하네스 (CLAUDE.md, hooks, skills, commands, agents)를 표준 구조로 성숙시키고(project-init), 그 품질을 6개 차원(정확성, 안전성, 완전성, 실행 가능성, 일관성, 검증 가능성)에서 채점받는(harness-eval) 단계입니다. superpowers의 3단(brainstorm, write-plan, execute-plan) 뒤에 4단 성숙화와 5단 평가를 붙이는 셈입니다.

1플러그인 2종 설치
Terminal, 전체 복사
claude plugin marketplace add https://github.com/whchoi98/project-init
claude plugin install project-init@project-init
claude plugin marketplace add https://github.com/whchoi98/harness-eval
claude plugin install harness-eval@harness-eval
claude plugin list

설치 후 새 Claude 세션을 시작하세요, superpowers 때와 같은 규칙입니다.

2성숙화, /project-init:init-project와 /project-init:sync-docs
Claude 세션 입력 (새 세션, worktree에서)
/project-init:init-project .

기존 프로젝트 감지 모드로 동작합니다. 이 캡스톤에서 손으로 만든 ops-interlock 훅과 /drill 스킬, RUNBOOK.md 같은 기존 하네스는 보존하고, 없는 것(문서 스캐폴딩, 시크릿 스캔 훅, 테스트 프레임워크, /review와 /test-all 커맨드 등)만 채웁니다. CLAUDE.md가 이미 있으면 덮어쓰기 전에 물어봅니다. 끝나면 이어서:

Claude 세션 입력
/project-init:sync-docs
출력 예시 (요약)
## Sync Report
### Quality Scores (Before -> After)
| ./CLAUDE.md          | C (62) | B (84) | +22 |
| ./lambda/CLAUDE.md   | F (12) | C (58) | +46 |
### Changes Made
- Files created: 4 / Files updated: 2 / Runbooks missing: 1
3평가, /harness-eval로 등급 받기
Claude 세션 입력
/harness-eval:quick
출력 예시
{"mode": "quick", "scores": {"overall": 7.2, "grade": "B"}, "checklist": {"pass": 13, "warn": 2, "fail": 1}}

ops-interlock은 harness-eval의 안전성 차원이 가장 좋아하는 종류의 장치입니다. 자율 에이전트와 가드레일을 함께 배포한 프로젝트가 높은 등급을 받습니다. 여유가 있다면 이 캡스톤에서 실제로 내린 결정을 기록으로 남기세요:

Claude 세션 입력
/add-adr interlock-first-safety
/add-runbook medic-drill
시간과 신뢰 가드 잔여 시간이 빠듯하면 설치와 /harness-eval:quick(30초 체크리스트)만으로 충분합니다, /project-init:init-project는 17단계라 수 분이 걸립니다. /harness-eval:standard의 동적 분석은 대상 프로젝트의 훅과 테스트를 실제로 실행합니다. 방금 본인이 만든 프로젝트라 안전하지만, 남의 저장소를 평가할 때는 --static-only 플래그가 예의이자 안전입니다.
OPTION DONE, 진행률 미집계
APPENDIX

컷라인과 트러블슈팅

비상용

90분 시점 기준으로 순서대로 적용하세요. 어떤 컷에서도 M3의 메딕 1차 드릴과 M5의 블라인드는 지킵니다.

조치절약
CUT 1M4 데몬 생략, 메딕은 수동 실행으로 진행 (인터록은 유지)~8분
CUT 2M2 수동 드릴을 주입까지만 하고 처방은 런북 눈으로 확인~5분
CUT 3M5 블라인드를 모드 2(THROTTLE) 고정으로 대체~5분
T트러블슈팅 표
증상원인처방
메딕이 원인을 못 찾음RUNBOOK.md 경로 또는 cdk-outputs.json 미존재worktree에서 두 파일 존재 확인, OPS_OUTPUTS 경로 점검
ENV_BREAK 복구 후에도 잠시 500함수 설정 반영 지연정상, medic의 검증 루프가 기다립니다 (최대 60초)
medic이 즉시 healthy로 끝남카오스가 다른 스택/리전에 주입됨OPS_OUTPUTS가 현재 worktree의 outputs인지 확인
claude가 돈/턴 한도로 중단--max-budget-usd 또는 --max-turns 도달INCIDENTS의 FAILED 기록 확인 후 상한 상향, 런북 신호를 더 구체화
INCIDENTS.md가 안 생김OPS_TARGET_API 미설정 또는 medic을 다른 폴더에서 실행export 재확인, worktree에서 실행
인터록이 치료 명령까지 차단BLOCK 정규식 과잉ops-interlock.sh의 패턴 확인, delete-function 뒤 공백 유지
블라인드가 매번 같은 모드RANDOM 시드 아님, 우연수 회 반복 또는 모드 지정 드릴로 3종 모두 검증

마무리, 챕터 소환 지도

캡스톤 장면소환된 챕터핵심 역량
brainstorm → 4-Phase 계획Ch1 + superpowers계획 우선, 요구사항으로 워크플로 조종
런북 병렬 작성Ch2서브에이전트 위임, 문서도 병렬로
메딕 도구 제한, IAM 이중화 원칙Ch3최소 권한 사고, Bedrock 모델 지정
PreToolUse 인터록, /drill 스킬Ch4훅은 가드레일이다, exit 2의 힘
medic.sh 전체Ch5헤드리스 3중 잠금, JSON 파싱, exit 게이트, 정기 실행
NEXT

Chapter 6 - Agent SDK

오늘 메딕은 60초마다 깨어나는 셸 루프였습니다. Chapter 6에서는 같은 메딕을 Agent SDK 상주 서비스로 승격해, 이벤트 구독과 커스텀 도구, 멀티 인시던트 병렬 처리까지 확장합니다. 셸이 프로토타입이었다면 SDK가 프로덕션입니다.