# Temporal 입문 — CloudOps 수집 흐름으로 이해하기

> 기준일: 2026-07-31 · 검증: `CODE`, `DOC`, 일부 `E2E-LOCAL`

이 문서는 Temporal 용어를 외우기 위한 자료가 아니다. CloudOps의 계정 수집을
예로 들어 **Temporal이 왜 필요하고, 각 구성 요소가 어떻게 협력하는지** 설명한다.
실제 클래스와 단계별 실패 동작은
[CloudOps 적용 흐름](02-cloudops-application-flow.md)에서 확인한다.

읽는 목적에 따라 다음 구간까지만 봐도 된다.

- **개념만 이해하기:** 1~6장
- **코드 작성 원칙까지 보기:** 7장
- **CloudOps 실행 구조와 운영 연결까지 보기:** 8~10장
- **추가 설계 과제 보기:** 11장

## 1. Temporal을 한 문장으로 설명하면

> Temporal은 오래 걸리는 업무가 어디까지 진행됐는지 기록하고, 실패하거나
> 프로세스가 재시작돼도 다음 작업을 이어 주는 실행 관리자다.

CloudOps 리소스 수집은 다음처럼 여러 단계로 이어진다.

```text
활성 계정 조회
  → 계정별 CSP 수집
  → 리소스 저장
  → 상태 반영
  → 관계 그래프
  → 리소스 매핑
  → WA 평가
  → 티켓 동기화
```

일반 프로세스만 사용하면 중간에 프로세스가 종료됐을 때 “어느 계정까지 끝났는지”,
“무엇을 다시 실행해야 하는지”, “후처리를 시작해도 되는지”를 애플리케이션이
직접 기록하고 복구해야 한다.

Temporal은 각 단계의 시작·성공·실패를 **실행 이력(Event History)**으로 남긴다.
워커가 재시작되면 이 기록을 바탕으로 아직 끝나지 않은 작업을 이어 간다.

다만 Temporal이 실제 수집이나 DB 저장을 하는 것은 아니다. Temporal은 일을
**기억하고 배정**하고, 실제 코드는 CloudOps 워커가 실행한다.

## 2. 구성 요소는 이렇게 협력한다

처음에는 다음 다섯 가지만 구분하면 된다.

| 구성 요소 | 쉬운 설명 | CloudOps 예 |
|---|---|---|
| Workflow | 어떤 순서로 진행할지 정하는 절차 | 전체 수집, 계정별 수집 |
| Activity | DB·CSP처럼 외부에 실제 영향을 주는 작업 | 계정 조회, CSP 호출, 리소스 저장 |
| Worker | Workflow와 Activity 코드를 실행하는 프로세스·Pod | Core, AWS, Azure, GCP 워커 |
| Task Queue | 어떤 Worker가 작업을 받을지 구분하는 논리적 대기열 | `core-collect`, `plugin-aws-plan` |
| Temporal Service | 실행 상태를 기억하고 다음 작업을 배정하는 서버 | Frontend·History·Matching |

한 계정의 AWS 리소스를 수집하는 과정을 단순화하면 다음과 같다.

```text
① API 또는 스케줄러
   “전체 수집을 시작해 줘”
          │
          ▼
② Temporal Service
   실행을 기록하고 첫 작업을 core-collect 큐에 넣음
          │
          ▼
③ Core Worker
   활성 계정을 찾고 계정별 Workflow를 시작
          │
          ▼
④ Temporal Service
   AWS 계획 생성·수집 Activity를 AWS 큐에 배정
          │
          ▼
⑤ AWS Worker
   AWS API를 호출하고 결과를 돌려줌
          │
          ▼
⑥ Core Worker
   결과 저장과 후처리를 진행
          │
          ▼
⑦ Temporal Service
   위 과정의 성공·실패와 다음 단계를 계속 기록
```

여기서 중요한 점은 두 가지다.

- API와 스케줄러는 Workflow를 **시작만** 한다. 요청이 수락되면 해당 프로세스가
  종료돼도 실행은 이어진다.
- Core Worker가 AWS Worker를 직접 호출하지 않는다. 모든 Worker가 Temporal의
  Task Queue를 통해 작업을 주고받는다.

즉 Temporal은 여러 워커와 스케줄러에 공통으로 필요한 **실행 순서, 상태 보존,
재시도, timeout, 취소, 관측**을 한곳에서 처리하는 매개체다.

## 3. 장애가 나면 무엇이 달라지는가

Temporal의 역할은 “어떤 일이든 성공시킨다”가 아니다. **실패한 위치와 재시도
상태를 잃지 않는 것**에 가깝다.

| 상황 | Temporal의 동작 |
|---|---|
| 시작 요청이 수락된 뒤 API가 종료됨 | Workflow는 계속 실행된다. |
| 해당 큐를 보는 Worker가 없음 | 작업은 사라지지 않고 Worker를 기다린다. |
| Activity 실행 중 Worker가 종료됨 | timeout 뒤 실패로 판단하고 정책에 따라 재시도한다. |
| Workflow Worker가 종료됨 | 다른 Worker가 실행 이력을 재생해 현재 상태를 복원한다. |
| Activity가 예외를 반환함 | retry policy에 따라 다시 배정할 수 있다. |
| Activity가 오류를 정상 결과로 반환함 | Temporal은 성공으로 본다. Workflow가 별도로 판정해야 한다. |

마지막 차이는 구현할 때 자주 놓친다.

```python
# 실패로 기록되어 재시도할 수 있다.
raise TemporaryProviderError(...)

# 호출 자체는 성공으로 기록된다.
return {"errors": ["provider timeout"]}
```

부분 성공을 정상 결과로 다룰 수는 있다. 다만 **재시도할 기술 오류**와
**상위 단계가 판단할 업무 결과**를 의도적으로 구분해야 한다.

## 4. 왜 멱등성이 가장 중요한가

**멱등성**은 같은 작업을 두 번 실행해도 결과가 한 번 실행한 것과 같게 유지되는
성질이다.

예를 들어 티켓 생성은 성공했지만 Worker가 Temporal에 완료 응답을 보내기 직전에
종료될 수 있다.

```text
Worker                 티켓 DB                 Temporal
  │  티켓 생성 요청       │                       │
  ├──────────────────────>│                       │
  │  티켓 #380 생성 완료  │                       │
  │<──────────────────────┤                       │
  X  완료 응답 전에 종료                          │
                                                  │ timeout
                                                  └─ Activity 재시도
```

Temporal은 티켓이 만들어진 사실을 알 수 없으므로 같은 Activity를 다시 실행한다.
이 때문에 “Temporal을 쓰면 정확히 한 번 실행된다”라고 이해하면 안 된다.

안전한 Activity는 다음 방식 중 하나를 사용한다.

- DB unique key와 upsert
- `create-or-get`
- 같은 업무 요청을 식별하는 idempotency key
- 이미 처리된 상태라면 아무것도 하지 않는 검사
- DB 변경과 이벤트 발행 사이의 transactional outbox

CloudOps의 알림 티켓은 `alert:{alert_id}`를 중복 방지 키로 사용한다.

> Temporal은 **다시 실행할 기회**를 제공한다. 다시 실행해도 **안전한지**는
> Activity가 보장해야 한다.

## 5. 부모·자식 Workflow는 왜 나누는가

계정마다 수집 시간과 실패 원인이 다르다. 전체 수집을 하나의 거대한 Workflow로
만들지 않고 계정별 자식 Workflow로 나누면 다음이 쉬워진다.

- 계정별 진행 상태와 실패 원인을 따로 확인한다.
- 한 계정의 실패를 다른 계정과 격리한다.
- 부모 Workflow의 실행 이력이 지나치게 커지는 것을 줄인다.
- 어떤 자식이 끝나야 다음 단계로 갈지 명시한다.

자식 실행에는 두 가지 업무 의미가 있다.

| 방식 | 의미 | CloudOps 예 |
|---|---|---|
| 완료까지 기다림 | 자식 결과가 다음 단계의 조건 | 계정별 수집, 관계 그래프 |
| 시작만 확인하고 분리 | 자식 완료를 부모 성공 조건에서 분리 | WA 평가, 리소스 매핑 |

CloudOps의 `ABANDON` 설정은 “실패해도 상관없다”는 뜻이 아니다. 부모 수집 완료와
후처리 완료를 **별개의 성공 기준**으로 본다는 뜻이다. 분리한 자식의 실패는 UI,
로그 또는 경보로 따로 추적해야 한다.

## 6. NATS와 Temporal은 역할이 다르다

Temporal이 NATS를 모두 대체한 것은 아니다.

| 필요한 것 | 실행 기반 | CloudOps 적용 |
|---|---|---|
| 여러 단계의 순서·대기·재시도·부모 자식 관계 | Temporal | 계정별 리소스 수집과 후처리 |
| 독립 이벤트의 저장·재전달·여러 소비자에게 전달 | NATS JetStream | CSP 이벤트 → 알림 → 티켓 |
| 비용·대시보드 집계 | 별도 집계 영역 | 이 문서 범위 밖 |

간단히 말하면 NATS는 **어떤 사건이 발생했다**는 이벤트 전달에 강하고,
Temporal은 **이 업무가 몇 단계까지 끝났다**는 실행 상태 관리에 강하다.

CSP webhook부터 alert와 티켓까지의 NATS 경로는
[CloudOps 적용 흐름](02-cloudops-application-flow.md)에 함께 정리돼 있다.

## 7. 구현할 때 지켜야 할 원칙

### 7.1 Workflow는 같은 이력에서 같은 결정을 내려야 한다

Workflow는 실행 이력을 다시 읽어 상태를 복구한다. 따라서 실행할 때마다 결과가
달라질 수 있는 동작을 Workflow 안에서 직접 사용하면 안 된다.

- 일반 `datetime.now()` 대신 `workflow.now()` 사용
- 입력 목록을 정렬해 호출 순서 고정
- DB·HTTP·CSP SDK 호출은 Activity로 이동
- 일반 random·UUID 대신 Temporal API를 사용하거나 Workflow 밖에서 생성

이를 **결정성(determinism)** 규칙이라고 한다.

### 7.2 Activity는 중복 실행을 견뎌야 한다

완료 응답 유실, Worker 종료, timeout 때문에 같은 Activity가 다시 실행될 수 있다.
DB 저장, 티켓 생성, 외부 API 호출에는 업무 중복 방지 장치를 둔다.

### 7.3 timeout과 retry를 함께 정한다

| 설정 | 답하는 질문 |
|---|---|
| Schedule-to-Start | Worker를 큐에서 얼마나 기다릴 것인가? |
| Start-to-Close | 한 번의 Activity 실행을 얼마나 기다릴 것인가? |
| Schedule-to-Close | 큐 대기와 모든 재시도를 합쳐 총 얼마를 허용할 것인가? |
| Heartbeat timeout | 진행 보고가 끊긴 작업을 언제 실패로 볼 것인가? |

무조건 긴 timeout이나 많은 재시도는 장애를 늦게 드러낼 수 있다. 정상 처리 시간과
CSP 제한을 근거로 함께 정한다. Activity에는 기본 재시도가 있지만 Workflow
Execution에는 기본 재시도가 없다.

### 7.4 긴 Activity는 진행 상태를 보고한다

현재 `collect_resources`는 최대 30분 실행될 수 있지만 heartbeat를 보내지 않는다.
Worker가 갑자기 종료되면 Temporal이 timeout까지 기다린 뒤에야 실패를 알 수 있다.

긴 수집은 region이나 페이지 단위로 heartbeat와 checkpoint를 남기고, 재시도 때
마지막 위치부터 이어 가거나 처음부터 안전하게 다시 실행할 수 있어야 한다.

### 7.5 비밀과 큰 데이터는 실행 이력에 그대로 넣지 않는다

Workflow와 Activity의 입력·결과는 실행 이력에 저장된다.

- 시작 주체와 모든 Worker에 같은 payload 암호화 codec을 적용한다.
- 오류 메시지와 stack trace에도 자격 증명을 남기지 않는다.
- 큰 리소스 원문은 요약하거나 claim-check로 외부 저장한다.

현재는 `TEMPORAL_ENCRYPTION_KEY`가 있을 때만 payload가 암호화된다. 운영의 키
주입·회전과 UI 복호화용 Codec Server는 별도 확인이 필요하다.

큰 payload는 History 전체 크기와 별개의 제한도 받는다. Temporal의 개별 payload
기본 한계는 2 MiB이고, 전송 구간에는 약 4 MiB의 gRPC 메시지 경계도 있다. 한 번의
Activity 결과가 이 값을 넘으면 History가 아직 작아도 실행이 실패할 수 있다.

CloudOps base에는 큰 값을 외부 저장소에 두고 History에는 참조만 남기는
claim-check가 있다. 하지만 기본은 비활성이며 endpoint가 설정됐을 때만 기본
256 KiB 이상 payload에 적용된다. 이 경우 외부 저장소는 단순 최적화가 아니라
Workflow 실행의 필수 의존성이 된다.

dev에서는 계정 하나의 권한 정보 Activity 결과가 2 MiB와 4 MiB를 모두 넘었는데도
성공한 실행이 관찰됐다. 현재 자료만으로는 claim-check가 활성화됐는지, Temporal
또는 proxy의 메시지 한계를 올렸는지 확정할 수 없다. 운영에서는 다음을 함께
확인해야 한다.

- API·Scheduler·모든 Worker가 같은 claim-check 설정과 저장소를 사용하는가
- 외부 payload 저장소의 장애·보존·삭제·암호화 정책이 있는가
- Temporal cluster와 중간 proxy의 payload·gRPC 한계가 얼마인가

### 7.6 Workflow 코드 변경은 진행 중인 실행과 호환돼야 한다

Activity나 Child Workflow의 순서를 바꾸면 새 Worker가 과거 이력을 재생할 때 다른
명령을 만들 수 있다. 배포 전 replay test, patching 또는 Worker Versioning으로
호환성을 확인한다.

## 8. CloudOps에서는 Worker와 스케줄러가 어떻게 실행되는가

```text
수동 API ───────────────┐
매일 01:00 스케줄러 ────┤
                        ▼
                Temporal Service :7233
                   │           │
          core-collect 큐      CSP별 큐
                   │           │
             Core Worker      AWS·Azure·GCP Worker
```

| 프로세스 | 역할 |
|---|---|
| Core API | 수동 수집 요청을 받아 Workflow 시작 |
| `core-scheduler` | 매일 01:00(KST)에 Workflow 시작 |
| Core Worker | `core-collect`에서 Workflow와 DB 중심 Activity 실행 |
| CSP Worker | CSP별 수집·계획·자격 증명 확인 Activity 실행 |

AWS·Azure·GCP Worker는 각각 한 프로세스 안에서 큐 세 개를 조회한다.

| Task Queue | 역할 |
|---|---|
| `plugin-{provider}` | 오래 걸리는 `collect_resources` |
| `plugin-{provider}-plan` | 수집 계획을 만드는 `make_plan` |
| `plugin-{provider}-heartbeat` | 짧은 `validate_credential` |

큐를 나누는 이유는 긴 수집이 몰렸을 때 짧은 계획 생성과 자격 증명 확인이 실행
기회를 잃지 않게 하기 위해서다. 한 CSP Pod가 종료되면 그 안의 Worker 세 개도
함께 사라지고, 새 Pod가 연결될 때까지 관련 작업은 큐에서 기다린다.

Core Worker는 queue 하나를 실행하므로 base의 공통 `run_worker`를 사용한다.
반면 각 CSP 프로세스는 Worker 세 개를 한 event loop에서 함께 실행하며 종료
신호 처리 코드를 플러그인 안에 따로 갖고 있다.

이 이탈에는 이유가 있다. 공통 `run_worker`는 호출할 때마다 event loop의 TERM·INT
signal handler를 설치한다. 한 프로세스에서 세 번 호출하면 뒤 handler가 앞
handler를 덮어써 첫 Worker들이 종료 신호를 받지 못할 수 있다. 그래서 CSP
플러그인은 Worker 세 개가 하나의 `stop_event`를 공유하도록 별도 bootstrap을
구현했다.

현재 AWS·Azure·GCP 구현의 timeout과 동시성 값은 우연히 같지만 base 기본값을
바꾼다고 플러그인에 자동 반영되지 않는다. 공통화하려면 “한 프로세스에서 여러
Worker를 등록하고 한 번만 signal handler를 설치하는” multi-worker runner가
먼저 필요하다.

`daily-collect`는 Temporal Schedule이 아니라 별도 `core-scheduler` 프로세스의
**APScheduler cron**이다. 메모리 저장소와 replica 1을 전제로 하며, 여러 Pod
사이의 분산 잠금은 없다. 스케줄러 replica를 늘리면 중복 Workflow를 막는 설계를
추가해야 한다.

상세 코드 위치와 실제 Task Queue 이름은
[CloudOps 적용 흐름의 실행 주체](02-cloudops-application-flow.md#12-실행-프로세스와-연결-관계)에서
확인한다.

## 9. gRPC와 Web UI는 서로 다른 입구다

```text
API·스케줄러·모든 Worker
          │ gRPC
          ▼
Temporal Frontend :7233

사람의 브라우저
          │ HTTP
          ▼
Temporal Web UI :8233
```

- `7233`: 프로그램이 Workflow를 시작하고 Task Queue를 조회하는 gRPC 포트
- `8233`: 사람이 실행 상태를 보는 Web UI의 HTTP 포트

Worker에서 Temporal Frontend로 **나가는 연결**을 만든다. Core Worker와 CSP
Worker가 직접 gRPC로 서로를 호출하거나, Worker Pod에 Temporal용 inbound 포트를
열 필요는 없다.

현재 client factory의 기본 주소는 `localhost:7233`, namespace는 `default`다.
코드에서 TLS와 인증 metadata를 직접 설정하는 부분은 확인되지 않았다. 다만
service mesh나 private network 같은 운영 구성은 저장소 밖에 있을 수 있으므로,
운영 연결이 평문이라고 단정할 수는 없다.

로컬 UI는 <http://localhost:8233>에서 볼 수 있다. 부모 Workflow를 검색한 뒤
`Relationships`에서 자식을 찾고, `Timeline`에서 Activity 순서를 보면 된다.
직접 실행해 캡처한 화면은 [HTML 흐름도](index.html)의 **실행 증거** 탭에 있다.

## 10. Kubernetes 종료 시간은 왜 Worker drain보다 길어야 하는가

Worker drain은 Pod가 종료될 때 새 작업을 받지 않고, 이미 실행 중인 Activity가
마무리될 시간을 주는 과정이다.

Kubernetes는 Pod 종료를 시작하면 `terminationGracePeriodSeconds`만큼 기다린다.
`preStop`도 이 시간 안에서 실행된다. 남은 프로세스가 제한 시간 안에 끝나지 않으면
KILL로 강제 종료한다.

```text
Kubernetes 종료 유예 시간
  > preStop 시간
  + TERM 전달 지연
  + Temporal Worker drain
  + 로그·연결 정리
  + 안전 여유
```

현재 코드의 Worker drain 기본값은 30초다. 주석은 “Kubernetes 기본 30초보다
짧게”라고 설명하지만 **30초는 30초보다 짧지 않다.** 실제 Pod도 기본값 30초를
쓴다면 drain을 마치기 전에 강제 종료될 수 있다.

`terminationGracePeriodSeconds` 전체를 drain만 쓰는 것도 아니다. kubelet의
`preStop` 실행, TERM이 Python event loop에 전달되는 시간, Worker 종료 뒤
DB·로그·네트워크 연결을 닫는 시간도 같은 예산에서 빠진다. 30초 Pod grace 안에서
30초 drain을 요청하면 나머지 단계에 쓸 시간이 0초다.

운영 Deployment manifest는 현재 작업공간에서 확인되지 않았다. 실제 값을 먼저
확인한 뒤 Pod 종료 유예 시간을 drain보다 충분히 길게 두거나 drain을 더 짧게
조정해야 한다.

```yaml
spec:
  terminationGracePeriodSeconds: 45  # 예시이며 측정 후 결정
```

```python
graceful_shutdown_timeout = timedelta(seconds=30)
```

graceful shutdown은 정상 배포를 돕지만 노드 장애나 OOM까지 막지는 못한다. 그래서
Activity 멱등성, heartbeat, checkpoint, 재시도가 함께 필요하다.

## 11. 더 깊이 볼 때

| 주제 | 현재 확인된 내용 | 다음 확인 |
|---|---|---|
| Workflow ID | 수동·예약 부모 ID에 임의 접미사 사용 | 업무 중복 방지용 ID와 conflict policy 설계 |
| 실행 이력 크기 | 계정별 Child로 분리하고 task별 저장 | 장기 실행은 Continue-As-New 검토 |
| 기본 제한 | History 51,200 events 또는 50 MB, 10,240 events 또는 10 MB부터 경고 | 운영 dynamic config와 실제 증가율 확인 |
| 개별 payload | 기본 2 MiB, gRPC 약 4 MiB 경계. dev의 대형 결과 성공 원인은 미확정 | claim-check 실제 활성화 또는 server/proxy override 확인 |
| CSP Worker 동시성 | 큐마다 기본 Activity 동시성 100 | Pod 자원·CSP quota·429를 함께 측정 |
| 다중 Worker bootstrap | CSP별 Worker 세 개가 별도 signal handling을 복제 | base에 multi-worker runner를 두고 기본값 drift 제거 |
| payload 암호화 | 키가 있을 때만 codec 활성화 | 운영 키 주입·회전·Codec Server 확인 |
| Workflow 배포 | 과거 이력과 코드 호환 필요 | replay test와 Worker Versioning 도입 |

코드를 변경할 때는 최소한 다음 네 질문에 답해야 한다.

1. 같은 Activity가 두 번 실행돼도 안전한가?
2. 이 오류는 재시도할 예외인가, 업무 결과인가?
3. 어떤 자식이 끝나야 다음 단계로 가는가?
4. 새 Workflow 코드가 이미 진행 중인 실행 이력과 호환되는가?

## 참고 자료

- [Temporal Workflow Definition](https://docs.temporal.io/workflow-definition)
- [Temporal Activities](https://docs.temporal.io/activities)
- [Temporal Retry Policies](https://docs.temporal.io/encyclopedia/retry-policies)
- [Temporal Worker Shutdown](https://docs.temporal.io/encyclopedia/workers/worker-shutdown)
- [Temporal Worker Deployments](https://docs.temporal.io/production-deployment/worker-deployments)
- [Temporal Web UI](https://docs.temporal.io/web-ui)
- [Kubernetes Pod termination](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination-flow)
