# 리소스 수집 이후 처리 흐름

CSP 계정이 등록된 순간부터 리소스가 수집되고, 관계가 맺어지고, 진단을 받고, 티켓으로 사람에게 전달되기까지 무엇이 어떤 순서로 도는지 정리한다. 각 단계마다 누가 실행하는지, 다음 단계로 넘어가는 조건이 무엇인지, 실패하면 어떻게 되는지를 함께 적는다.

비용과 대시보드처럼 데이터 집계가 주인 영역은 범위 밖이다.

## 0. 두 개의 라인

리소스를 다루는 경로는 성격이 다른 두 줄기다.

| | 수집 라인 | 이벤트 라인 |
|---|---|---|
| 실행 기반 | Temporal | NATS JetStream |
| 시작 | 스케줄 또는 사용자 요청 | CSP가 보내는 webhook |
| 주기 | 배치 | 실시간 |
| 하는 일 | 계정 전체를 훑어 상태를 최신화 | 개별 사건을 받아 alert로 묶음 |
| 도착점 | 티켓 | 티켓 |

수집은 Temporal로 옮겨왔고, 이벤트 라인은 아직 NATS 위에 있다. 두 줄기는 서로 독립적으로 돌다가 티켓에서 만난다.

```
[수집 라인]  스케줄/요청 → 계획 → 수집 → 적재 ┐
                                              ├→ 관계 → 매핑
                                              ├→ 진단 → 티켓 ┐
                                                              ├→ 사람
[이벤트 라인] webhook → 정규화 → alert ───────────→ 티켓 ┘
```

## 1. 수집 라인

### 1.1 흐름을 시작시키는 것들

| 시작 계기 | 시작되는 것 | 결과를 기다리나 | 비고 |
|---|---|---|---|
| 매일 01:00 스케줄 | 수집 전체 실행 | 아니오 | 활성 계정을 스스로 찾음 |
| 사용자의 수집 요청 | 수집 전체 실행 | 아니오 | 요청당 계정 100개까지 |
| 계정 삭제 | 관계 정리 | 아니오 | 10초 뒤에 시작 |
| 계정 삭제 | 진단 재평가 | 아니오 | 15초 뒤에 시작 |
| 자격증명 확인 | 자격증명 검증 | 예 | 화면에서 대기, 8초 예산 |

스케줄은 매일 01:00 기준이다. 다만 스케줄 기능 자체가 기본 꺼짐이라, 실제로 도는지는 그 파드에 스케줄이 켜져 있는지에 달려 있다.

사용자 요청도 스케줄과 같은 경로를 탄다. 예전에는 손으로 돌린 수집이 뒤따르는 처리를 전부 건너뛰어서 자동 수집과 결과가 달라졌다. 지금은 입구만 다르고 그 뒤는 같다.

계정을 지울 때 두 가지가 따라온다. 관계 데이터를 정리하는 것과, 그 고객의 진단을 다시 돌리는 것이다. 둘 다 몇 초 뒤에 시작하는데, 삭제가 저장되기 전에 시작하면 지운 리소스가 아직 살아 있는 것처럼 보이기 때문이다.

진단 재평가는 실제로 지워진 리소스가 있을 때만 돈다. 지워진 게 없으면 다시 평가해도 결과가 같아서다.

### 1.2 전체 실행이 시작될 때

| 순서 | 하는 일 | 실행 주체 | 시간 제한 | 재시도 |
|---|---|---|---|---|
| 1 | 활성 계정 목록 확보 | core | 2분 | 3회 |
| 2 | 전 계정을 예약 상태로 표시 | core | 2분 | 3회 |

사용자 요청으로 시작한 경우에는 1번을 건너뛴다. 요청에 이미 계정 목록이 들어 있기 때문이다.

2번을 자식보다 먼저 하는 이유가 있다. 계정은 한 번에 20개까지만 동시에 돈다. 순서를 기다리는 계정이 이전 수집 결과를 그대로 달고 있으면 사용자가 요청이 씹혔다고 오해한다. 그래서 시작하자마자 전부 한 번에 예약으로 바꾼다.

넘어가는 조건은 계정 목록을 얻는 것이다. 목록을 못 얻으면 실행 전체가 실패한다.

### 1.3 계정 하나가 도는 순서

계정마다 별도 실행이 하나씩 붙는다. 안에서 이 순서로 돈다.

| 순서 | 하는 일 | 실행 주체 | 시간 제한 | 재시도 |
|---|---|---|---|---|
| 1 | 무엇을 어떻게 긁을지 계획 | 플러그인 (계획 전용 줄) | 10분 | 3회 |
| 2 | 계정을 수집 중으로 표시 | core | 1분 | 3회 |
| 3 | 계획을 작업 목록으로 펼침 | core | 2분 | 3회 |
| 4 | 작업마다 리소스 수집 | 플러그인 (수집 줄) | 30분 | 3회 |
| 5 | 작업 결과를 저장 | core | 5분 | 제한 없음 |
| 6 | 계정을 최종 상태로 마감 | core | 1분 | 3회 |

4번과 5번은 작업 하나마다 짝으로 붙어 동시에 여러 벌 돈다. 한 작업이 수집을 마치면 그 결과만 바로 저장한다. 계정 전체를 모아서 한 번에 저장하지 않는데, 큰 계정에서 실행 이력이 감당 못 할 만큼 커지기 때문이다.

계획을 세우는 줄과 실제 수집하는 줄이 나뉘어 있다. 큰 계정 하나가 수집 작업을 수백 건 쏟으면 뒤에 들어온 계획 요청이 순서를 기다리다 굶어 죽던 문제 때문이다.

수집 시간 제한이 30분으로 넉넉한 것도 사고 때문이다. 권한 정보처럼 리소스마다 추가 호출이 붙는 수집은 10분을 넘겼고, 그 때문에 계정 전체가 실패했다.

**5번만 재시도 제한이 없다.** 다른 단계는 3회로 끊는데 저장 단계만 제한이 없어서, 고칠 수 없는 실패가 나면 계속 반복된다. 그래서 데이터 자체가 잘못된 경우는 재시도하지 않고 즉시 멈추도록 따로 처리해뒀다. 그 밖의 실패는 무한히 재시도된다. 의도인지 빠진 것인지는 코드에 근거가 없다.

### 1.4 계정 상태가 바뀌는 순서

```
예약됨 → 수집 중 → 성공 / 경고 / 실패
```

마지막 상태는 이렇게 갈린다.

| 상태 | 조건 |
|---|---|
| 실패 | 작업이 예외로 죽었거나, 플러그인이 치명적 오류를 보고함 |
| 경고 | 권한 부족이나 호출 제한처럼 외부 요인으로 일부만 못 긁음 |
| 성공 | 나머지 전부. 긁을 게 없어서 0건인 경우도 포함 |

이 결과는 Temporal 화면에서 조건 검색도 된다. 실행을 하나씩 열어보지 않고 실패한 계정만 골라볼 수 있다.

**중요한 함정이 있다.** 플러그인이 예외를 던지지 않고 "빈 결과 + 오류 표시"로 끝내는 경우가 있다. 예전에는 이게 깨끗한 성공처럼 보였다. 지금은 결과에 담긴 오류 등급을 읽어 실패로 잡아낸다.

### 1.5 계정 단계에서 실패하면

| 실패 지점 | 그 뒤 동작 |
|---|---|
| 계획 요청이 재시도를 다 쓰고 실패 | 계정을 실패로 마감하고 그 계정 실행도 실패로 끝냄. 수집 이하 전부 건너뜀 |
| 계획이 오류를 담아서 돌아옴 | 계정을 실패로 마감하고 조기 종료. 수집 중 표시도 안 함 |
| 수집 작업 하나가 실패 | 형제 작업은 계속 진행. 계정은 긁을 수 있는 것을 다 긁고 실패 건수만 보고 |
| 계정 하나가 통째로 실패 | 다른 계정은 계속 진행 |

계획이 오류를 담아 돌아오는 경우 하나만 짚어둘 것이 있다. 판정 조건이 "오류가 있는가" 하나뿐이라, 작업 목록을 정상적으로 채워 보내면서 오류도 함께 실으면 그 계정은 **수집 가능한 작업이 있어도 전부 건너뛰고 실패로 끝난다.** 부분 실패를 그렇게 보고하는 플러그인이 있다면 의도하지 않은 결과가 나온다.

계정끼리 실패를 격리하는 이유도 있다. 한 계정 실패가 실행 전체를 실패시키면, 아직 수집 중이던 나머지 계정이 전부 강제 종료된다.

### 1.6 모든 계정이 끝나는 지점

여기가 분기점이다. 모든 계정의 적재가 끝난 뒤에야 후처리가 시작된다. 데이터가 반쯤 들어온 상태에서 관계를 맺거나 진단을 돌리면 결과가 틀리기 때문이다.

후처리는 세 갈래인데, 하나만 결과를 기다린다.

| 갈래 | 기다리나 | 시작 시점 |
|---|---|---|
| 진단 | 아니오 | 분기점 직후 바로 |
| 관계 정리 | 예 | 분기점 직후 바로 |
| 매핑 | 아니오 | 관계 정리가 끝난 뒤 |

매핑만 뒤로 밀린 이유가 핵심이다. 매핑 규칙이 관계 정리에서 확정되는 값을 조건으로 쓴다. 기다리지 않으면 첫 사이클에서는 매칭이 안 되고 다음 날에나 맞아 들어간다.

관계 정리가 실패해도 매핑은 그대로 진행한다. 관계가 실패했다고 매핑까지 멈추면 손해가 더 크고, 다음 사이클에 자연히 복구된다.

이 순서는 실제 실행 기록으로도 확인된다.

```
관계 정리 종료  18.837
매핑 시작       18.851
```

### 1.7 관계 정리

리소스끼리의 연결 관계를 만들고, 그걸로 각 리소스가 어느 VPC에 속하는지 확정한다. 고객 단위로 돈다.

| 순서 | 하는 일 | 범위 | 시간 제한 | 재시도 |
|---|---|---|---|---|
| 1 | 관계 추출 | 계정마다 | 5분 | 3회 |
| 2 | 관계 확정 | 고객당 한 번 | 2분 | 3회 |
| 3 | VPC 소속 반영 | 고객당 한 번 | 2분 | 3회 |

동시성 규칙이 특이하다.

- 한 고객 안의 계정들은 **순서대로** 처리한다. 관계 데이터가 고객별로 나뉘어 저장되어 있어서, 같은 고객의 계정을 동시에 처리하면 서로 충돌한다.
- 고객끼리는 **동시에** 처리한다. 저장 영역이 달라 안전하다. 최대 8명까지.
- 2번과 3번은 고객당 딱 한 번이다. 계정마다 부르면 안 되는 작업이다.

이 단계는 AWS, Azure, GCP 계정만 대상이다. 나머지는 처리 절차가 없어서 넣지 않는다.

한 고객이 실패해도 다른 고객은 계속 진행한다.

### 1.8 매핑

아직 어느 어플리케이션에도 속하지 않은 리소스를 규칙에 걸어 소속을 확정한다. 고객당 한 번, 최대 8명 동시, 5분 제한에 3회 재시도다.

여기는 원래 NATS로 신호를 받아 돌던 자리다. Temporal로 옮기면서 그 신호가 사라져 매핑이 아예 안 걸리던 시기가 있었고, 지금은 분기점 뒤에서 직접 부르는 방식으로 복원했다.

### 1.9 진단과 티켓 발행

| 순서 | 하는 일 | 범위 | 시간 제한 | 재시도 |
|---|---|---|---|---|
| 1 | Well-Architected 평가 | 고객마다 | 15분 | 3회 |
| 2 | 티켓 정합 | 고객마다 | 15분 | 3회 |

2번은 1번이 성공했을 때만 이어진다. 평가가 실패하면 그 고객의 티켓은 건드리지 않는다.

티켓 정합은 평가 결과 중 문제가 있는 항목을 기존 티켓과 맞춘다. 새 문제는 티켓을 만들고, 이미 있는 문제는 그대로 두고, 해결된 문제는 닫는다. 평가 점수는 건드리지 않고 티켓만 다룬다.

최대 8명 동시다. 평가 자체가 내부에서 여러 검사를 동시에 돌리기 때문에 낮게 잡았다.

## 2. 수집기가 하는 일

계획과 수집은 core가 아니라 CSP별 수집기가 맡는다. 세 벌이 있고 AWS, GCP, Azure를 하나씩 담당한다.

### 2.1 배치

수집기 하나는 프로세스 하나로 뜨는데, 그 안에서 줄을 셋으로 나눠 각각 받는다.

| 줄 | 받는 일 |
|---|---|
| 수집 | 리소스 긁기 |
| 계획 | 무엇을 긁을지 정하기 |
| 검증 | 자격증명 확인 |

**줄을 나눈 것은 실제 장애 대응이다.** 하나로 쓰던 시절, 큰 계정의 수집이 자리를 전부 차지하면 계획 요청이 화면에서 "예정됨" 상태로 수십 분을 대기했다. 자격증명 검증은 8초 예산이라 아예 전멸했다. 줄을 나누니 서로 굶기지 않는다.

수집기는 흐름 자체를 소유하지 않는다. 순서를 정하는 것은 전부 core이고, 수집기는 불리면 일하는 쪽이다. 그래서 둘 사이 계약은 **줄 이름과 작업 이름 문자열 두 개뿐**이다. 이 문자열이 어긋나면 요청이 아무에게도 안 잡히고 통째로 사라진다. 현재는 9개 줄 이름과 3개 작업 이름이 모두 정확히 맞는다.

동시 처리 상한은 줄마다 100이다. 프로세스 하나가 최대 300까지 동시에 잡을 수 있다는 뜻이다.

### 2.2 계획 단계

등록된 수집 대상 전체에서 요청 범위에 맞는 것만 골라, 리전마다 작업 하나씩으로 펼친다. 작업 하나가 곧 리전 하나다. 전 세계 공통인 서비스는 리전 구분 없이 작업 하나다.

리전을 정하는 우선순위가 CSP마다 다르다.

| | AWS | GCP | Azure |
|---|---|---|---|
| 1순위 | 요청에 지정한 리전 | 요청에 지정한 리전 | 요청에 지정한 리전 |
| 2순위 | 계정 단위 지정 | 계정 단위 지정 | 계정 단위 지정 |
| 3순위 | 리소스가 실제 있는 리전만 조회 | 활성 리전 전체 조회 | 없음 |

**AWS만 리소스가 있는 곳을 먼저 찾는다.** 조회 결과가 0이면 "어디에도 없다"로 보고 작업을 아예 만들지 않는다. 낭비를 줄이는 장치다. 이 조회가 실패하면 리전 목록 조회로 물러선다.

**Azure는 리전을 나누지 않는다.** 구독 단위로 전체를 받아온 뒤 결과에서 리전을 걸러낸다. 그래서 작업이 리전 수만큼 늘어나지 않는다.

수집 대상 규모는 이렇다.

| | 그룹 | 타입 |
|---|---|---|
| AWS | 29 | 67 |
| GCP | 27 | 72 |
| Azure | 34 | 36 |

### 2.3 수집 단계

작업 하나가 리전 하나를 훑는다. 한꺼번에 다 받아 쌓지 않고 페이지 단위로 당겨온다.

실패 범위가 두 층으로 나뉜다.

| 실패 범위 | 동작 |
|---|---|
| 리소스 1건 | 그 건만 실패로 표시하고 나머지는 계속 |
| 리전 전체 | 즉시 중단. 그 전까지 긁은 것은 유지 |

리소스가 0건인 리전은 아무것도 보고하지 않는다. 예전에는 빈 리전도 실패 1건처럼 세어져서 숫자가 부풀었다.

### 2.4 실패 등급

이게 계정 상태를 가르는 핵심이다. 실패를 등급으로 나누고, 그 등급이 계정 상태로 번역된다.

원인을 15가지로 나누고 각각에 등급을 못박아 뒀다. 등급을 가르는 기준은 **우리 잘못인가**다.

**경고 (9가지)** 수집은 실패했지만 시스템 잘못이 아니다. 고객 환경이거나 일시적인 문제다.

| 원인 | 예 |
|---|---|
| 권한 부족 | 고객이 조회 권한을 안 줌 |
| 호출 제한 | CSP가 요청량을 제한 |
| 서비스 일시 오류 | CSP 쪽 5xx |
| 네트워크 오류 | 연결 실패, 응답 없음 |
| 리소스 없음 | 목록에는 있었는데 상세 조회 때 이미 지워짐 |
| 비활성 | 리전이나 구독이 꺼져 있음 |
| 미지원 작업 | 그 리전이나 계정에서 안 되는 조회 |
| 지원 등급 필요 | 상위 지원 계약이 있어야 열리는 정보 |
| 중복 리소스 | 수집은 됐는데 뒤처리에서 걸림 |

**오류 (5가지)** 진짜 실패다. 사람이 조치해야 한다.

| 원인 | 예 |
|---|---|
| 작업 중단 | 수집을 시작조차 못 함 |
| 인증 거부 | 자격증명 형식은 맞는데 거부됨 |
| 자격증명 무효 | 형식 자체가 잘못됨. 필드 누락 등 |
| 파라미터 오류 | 우리가 잘못된 값을 보냄 |
| 내부 오류 | 수집기 코드의 버그 |

**분류 실패** 위 어디에도 안 걸리면 오류로 본다. 안전한 쪽을 기본값으로 잡은 것이다. 이게 자주 나오면 분류 목록을 보강해야 한다는 신호다.

**권한 부족을 경고로 두는 것이 중요하다.** 고객이 일부 권한을 안 준 것 때문에 계정 전체가 실패로 찍히면 안 되기 때문이다. 다만 역할을 넘겨받는 단계에서 권한이 없으면 그건 자격증명 자체의 문제라 오류로 올린다.

등급은 수집기가 마음대로 정하지 못한다. 원인 종류마다 등급이 고정되어 있고, 예외를 두는 통로가 없다. 등급을 나눠야 하는 상황이면 원인 종류를 새로 만드는 방식이다. 이 목록은 세 수집기가 공유하므로 추가할 때도 공통 라이브러리를 고쳐야 한다.

자격증명이 틀린 경우는 수집 전에 걸린다. 이때는 리소스를 하나도 보고하지 않고 "0건 예정 + 오류 1건"만 남긴다. 실패 숫자가 부풀지 않게 하려는 것이다.

### 2.5 진단용 지표 수집

수집기는 리소스 정보만 긁는 게 아니라 **성능 지표도 함께 담아온다.** 이게 나중에 진단의 입력이 된다.

| | 대상 | 무엇을 |
|---|---|---|
| AWS | 5종 | 최근 30일 일별 평균과 최대. EC2 CPU만 14일 상위값 |
| GCP | 3종 | 최근 30일 일별 평균과 최대 |
| Azure | 2종 | 최근 30일 일별 평균과 최대 |

**수집기는 판정하지 않는다.** 이 원칙이 세 수집기 모두에 명시돼 있다. 임계값과 비교하거나 좋다 나쁘다를 정하는 것은 전부 진단 쪽 몫이고, 수집기는 벤더가 준 값을 그대로 담기만 한다. 예외가 하나 있는데, 상위값을 벤더가 안 주는 GCP와 Azure에서만 수집기가 직접 계산한다. 그것도 집계까지지 판정은 아니다.

지표 수집이 실패해도 리소스 수집은 성공한다. 지표는 있으면 좋은 것이지 필수가 아니라서다.

한 가지 주의할 점이 있다. 지표는 리소스 하나마다 별도 호출이 붙는다. 리소스가 많은 계정에서는 호출 제한에 걸리거나 비용이 늘 수 있다.

## 3. 이벤트 라인

CSP가 보내는 사건을 실시간으로 받는 경로다. Temporal은 이 라인에 관여하지 않는다.

### 3.1 수신

전용 주소로 받는다. 주소에 들어 있는 토큰 자체가 권한이라 별도 로그인이 필요 없다.

| 항목 | 값 |
|---|---|
| 본문 크기 상한 | 1MB |
| 응답 목표 | 200밀리초 이내 |
| 등록되지 않은 주소 | 404, 이벤트를 남기지 않음 |
| 본문이 JSON 객체가 아님 | 400 |

받은 원본은 그대로 큐에 넣고, 저장이 확인된 뒤에만 성공으로 응답한다. 저장에 실패하면 요청 자체가 실패한다. 로컬에 쌓아두는 완충 장치는 없다.

AWS 구독 확인 요청은 여기서 단락된다. 확인 주소가 정해진 형태일 때만 호출하는데, 본문이 인증 없이 들어오는 값이라 아무 주소나 열면 위험하기 때문이다.

짚어둘 것이 두 가지 있다. 이 주소에는 호출량 제한이 없고, 메시지 서명 검증도 없다. 토큰만으로 충분하다는 판단인지 다른 계층에서 하는지는 코드에 근거가 없다.

### 3.2 정규화

소스마다 형식이 달라서 해당 플러그인이 우리 공통 형식으로 바꾼다. 요청을 보내고 응답을 받는 두 번의 왕복이다.

현재 붙어 있는 플러그인은 다섯이다. CloudWatch, PHD, SNS, Grafana, Prometheus.

어느 단계도 다음 단계의 응답을 기다리지 않는다. 보내고 바로 확인 처리한다.

재전달 규칙은 이렇다.

| 항목 | 값 |
|---|---|
| 최대 전달 횟수 | 5회 |
| 응답 대기 | 300초 |
| 재시도 간격 | 300, 600, 900, 1500초 |

마지막 시도에서도 실패하면 조용히 버리지 않는다. 수신 원장에 오류로 남긴 뒤 처리를 끝낸다. 그래야 나중에 찾을 수 있다.

정규화 결과에 필수 항목이 빠져 있으면 형식 오류로 마감한다. 일시적 오류라고 표시해 오면 다시 시도한다.

리소스 식별자로 우리 자산을 찾아 연결한다. 못 찾으면 연결되지 않은 상태로 남긴다.

### 3.3 alert로 묶기

같은 원인이면 하나로 묶는다. 묶는 기준은 출처와 리소스와 규칙 조합이다. 같은 문제로 알림이 100번 와도 alert은 하나다.

판정은 네 갈래다.

| 들어온 것 | 기존 alert | 결과 |
|---|---|---|
| 발생 | 있음 | 기존에 붙임 |
| 발생 | 없음 | 새로 만듦 |
| 복구 | 있음 | 해소 |
| 복구 | 없음 | 무시 |

알 수 없는 종류가 오면 아무것도 하지 않는다.

시간 윈도우는 적용되지 않는다. 필드는 있지만 쓰이지 않는다. 즉 아주 오래된 문제와 방금 생긴 문제가 같은 조합이면 한 덩어리가 된다.

### 3.4 티켓 만들기와 자동 닫기

alert 상태가 바뀌면 티켓에 반영된다.

| 상황 | 동작 |
|---|---|
| 새 alert | 티켓 생성. 담당자는 alert 대응자로 채움. 알림은 보내지 않음 |
| 같은 alert 재수신 | 마지막 관측 시각만 갱신. 상태와 담당자는 보존 |
| 복구 알림 | 티켓 자동 완료. 알림 발송 |
| 이미 닫힌 티켓 | 아무것도 하지 않음 |

생성 이벤트를 놓쳐서 티켓이 없는데 복구만 들어오는 경우도 있다. 그때는 alert을 다시 읽어 티켓을 만들면서 바로 닫는다.

중복 방지 장치가 세 겹이다. 큐 차원의 중복 제거, 이미 끝난 티켓은 건드리지 않는 판정, 그리고 동시에 두 개가 처리해도 하나만 통과하는 조건부 갱신이다.

## 4. 두 라인이 만나는 곳

티켓이다. 진단에서 온 티켓과 alert에서 온 티켓이 같은 목록에 쌓인다. 출처는 티켓에 표시된다.

사용자가 티켓 상태를 바꿀 수 있는 조합은 이렇다.

| 현재 | 바꿀 수 있는 상태 |
|---|---|
| 열림 | 진행 중, 완료, 무시 |
| 진행 중 | 완료, 무시, 열림 |
| 완료 | 진행 중, 열림, 무시 |
| 무시 | 진행 중, 열림 |

재열림 상태는 시스템만 만들 수 있고 사용자가 지정할 수 없다.

사용자가 완료로 바꾸면 두 가지가 따라온다.

- alert에서 온 티켓이면 원본 alert도 함께 해소한다. 진단에서 온 티켓은 하지 않는다.
- 완료 알림을 보낸다.

서로 되울리지 않게 막아뒀다. 티켓이 alert을 닫을 때 그 alert이 이미 닫혀 있으면 조용히 넘어간다. 반대로 alert이 자동으로 티켓을 닫는 경로는 alert을 다시 건드리지 않는다.

## 5. 실패했을 때 무엇이 보이나

### 5.1 실패는 대부분 조용하다

계정 하나가 아무것도 못 긁어도 전체 실행 결과에는 "실패 계정 0건"으로 나온다. 전체 실행이 세는 것은 실행 자체가 죽은 계정뿐이고, 작업이 전부 실패한 계정은 실행으로는 정상 종료로 보이기 때문이다.

실제로 재현해보면 이렇게 나온다.

```
전체 결과: 계정 4건, 실패 0건

계정별
  A 계정   저장   0건   실패 3건    ← 아무것도 못 긁음
  B 계정   저장  12건   실패 0건
  C 계정   저장  12건   실패 0건
  D 계정   저장  12건   실패 0건
```

그러니 전체 결과만 보면 안 된다. 계정별 상태나 검색 조건으로 걸러 봐야 한다.

### 5.2 알려주는 장치가 없다

실패를 알아채는 방법이 전부 들여다보는 쪽이다. 먼저 알려주는 것은 없다.

| 방법 | 무엇을 알 수 있나 |
|---|---|
| 계정 목록 화면 | 계정별 수집 상태 |
| Temporal 화면 | 실패한 수집만 골라보기 |
| 로그 | 개별 실패 내용 |

알림이 나가는 영역은 티켓과 예산 두 곳뿐이다. **수집 실패는 알림 경로에 걸려 있지 않다.** 계정 상태를 읽어 알림을 만드는 코드가 없다.

지표를 내보내는 배선도 없다. 실행을 가로채는 자리는 있는데 감사 기록용 하나만 붙어 있다.

정리하면 야간 수집에서 계정 절반이 실패해도, 누군가 화면을 열어보기 전까지는 아무도 모른다.

### 5.3 실제로 방치된 사례가 있다

개발 환경을 확인해보니 **수집 중 상태로 두 달 가까이 멈춰 있는 계정**이 있었다. 예약된 상태로 남아 한 번도 수집되지 않은 계정도 여럿이다.

수집 중으로 멈춘 것은 수집이 시작되고 끝나지 않았다는 뜻이다. 그 계정은 그 이후로 데이터가 갱신되지 않는데, 화면에는 오류가 아니라 진행 중으로 보인다.

왜 멈췄는지는 지금 추적하기 어렵다. 실행 이력 원장이 아직 배포되지 않았고, Temporal 쪽 실행 기록은 보관 기간이 지나면 사라진다.

### 5.4 기다리지 않는 것들의 실패는 더 조용하다

진단과 매핑은 결과를 기다리지 않고 띄워놓는다. 그래서 이 둘이 실패해도 수집 실행은 성공으로 끝난다. 실패했다는 사실은 각각의 실행을 따로 열어봐야 보인다.

### 5.5 실행 이력 원장

최근에 붙은 장치다. 최상위 실행마다 시작과 종료를 별도로 기록해둔다. 무엇이 언제 시작해서 어떻게 끝났는지, 어떤 계기로 시작됐는지가 남는다.

자식 실행은 기록하지 않는다. 부모의 일부로 보기 때문이다.

원장 기록이 실패해도 본 작업은 막지 않는다. 감사 기록 때문에 실제 수집이 멈추면 안 된다는 판단이다.

기록을 남기지 못하고 끝난 실행을 나중에 보정하는 장치도 준비돼 있는데, 아직 주기적으로 도는 배선은 없다. 지금은 필요할 때 수동으로 돌려야 한다.

## 6. 아직 정해지지 않은 것

흐름을 정리하면서 드러난, 코드에 근거가 없는 자리들이다.

**티켓 처리 이후가 비어 있다.** 사용자가 티켓을 완료로 바꾼 뒤 그 리소스를 다시 확인하는 경로가 없다. 조치가 반영됐는지는 다음 정기 수집이 끝나야 알 수 있어서 최대 하루가 빈다. 티켓과 alert 어느 쪽에도 수집이나 진단을 다시 부르는 연결이 없다.

**저장 단계의 재시도 제한이 없다.** 다른 단계는 모두 3회인데 저장만 제한이 없다. 의도인지 빠진 것인지 근거가 없다.

**계획 실패 판정이 넓다.** 작업 목록이 정상인데 오류가 함께 실려 오면 수집 가능한 것까지 전부 건너뛴다.

**매핑과 관계 정리의 대상 고객이 다르다.** 매핑은 수집된 전 고객을 대상으로 하고 관계 정리는 세 CSP 계정이 있는 고객만 대상이다. 두 집합이 어긋나는 고객에서 VPC 조건 규칙이 계속 매칭되지 않는지에 대한 판단이 없다.

**자격증명 검증의 시간 제한이 무시된다.** 호출하는 쪽이 제한 시간을 넘기는데 받는 쪽이 쓰지 않는다. 실제 상한은 8초 하나뿐이라, 대량 등록에서 이게 맞는 값인지 확인이 필요하다.

**alert 묶음에 시간 제한이 없다.** 아주 오래된 문제와 새 문제가 한 덩어리가 될 수 있다.

**계정 삭제로 도는 진단 재평가만 계기 정보를 남기지 않는다.** 다른 시작 지점은 모두 남긴다. 원장에서 이 실행만 계기가 비어 보인다.

수집기 쪽에서 나온 것들은 아래와 같다.

**CSP 이름이 줄 이름을 만든다.** 수집기를 부를 때 계정의 CSP 값을 그대로 줄 이름에 끼워 넣는다. 그래서 예상 밖의 값이 들어오면 아무도 듣지 않는 줄로 요청이 가고, 그 요청은 조용히 시간만 채우다 끝난다. 지금 확인된 위험 지점이 셋이다.

- 저장소는 세 CSP 외에 온프레미스 값도 허용하는데, 수집 대상을 고르는 조회에는 CSP 필터가 없다
- 자격증명 확인 요청은 CSP 값을 자유 문자열로 받고, 설명에는 우리가 쓰지 않는 이름이 예시로 적혀 있다
- 값이 아예 없으면 AWS로 간주하는 기본값이 있다

여기에 자격증명 확인은 실패 원인을 감추고 일반 메시지만 돌려준다. 줄 이름이 틀려서 아무도 못 받은 것인지, 진짜 자격증명이 틀린 것인지 화면에서 구분되지 않는다.

**수집 결과를 통째로 돌려준다.** 리전 하나에서 긁은 리소스를 전부 한 덩어리로 반환한다. 나눠 보내거나 잘라내는 장치가 수집기 쪽에 없다. 큰 값을 밖에 두는 기능이 있긴 한데 기본이 꺼짐이라, 리소스가 아주 많은 리전에서 한계를 넘길 수 있는지 확인이 필요하다.

**수집 범위를 겹쳐서 요청하면 같은 작업이 여러 번 생긴다.** 중복을 걸러내는 곳이 없다.

**수집 대상 등록을 빠뜨리면 조용히 안 긁힌다.** 오류가 나지 않고 그 리소스만 결과에서 빠진다.

**GCP는 리전 조회가 실패해도 계획이 성공한다.** 실패를 삼키고 리전 구분 없는 작업 하나로 떨어진다. 문제는 수집 단계에 가서야 드러난다.

## 7. 직접 확인하는 방법

이 문서의 순서는 실제로 돌려서 확인할 수 있다. DB도 CSP 자격증명도 없이 로컬에서 재현된다.

```bash
brew install temporal
temporal server start-dev --db-filename ./.local/temporal-dev.db
```

그다음 `tools/local-trace.py`를 돌리면 운영과 같은 흐름 정의가 로컬에서 실행되고, http://localhost:8233 에서 실행 트리를 볼 수 있다. 실패했을 때의 동작도 재현된다.

```bash
# 정상 흐름
PYTHONPATH=src venv/bin/python <이 폴더>/tools/local-trace.py

# 한 계정의 수집이 오류를 담아 돌아오는 경우
FAIL_MODE=abort RUN_SUFFIX=case1 PYTHONPATH=src venv/bin/python <이 폴더>/tools/local-trace.py

# 한 계정의 수집이 예외로 죽는 경우
FAIL_MODE=raise RUN_SUFFIX=case2 PYTHONPATH=src venv/bin/python <이 폴더>/tools/local-trace.py
```

Temporal 자체가 처음이면 `01-temporal-개념정리.md`를 먼저 읽는다.
