pseudo는 Kubernetes 환경에서 동작하는 Python FastAPI 기반 API 애플리케이션 개발을 위한 뼈대 설계와 health 관리 등을 위한 구현과 시험을 다룬다.
외부 데이터소스(DB, MQ 등)에 대한 커넥션 풀을 관리하며, healthz sidecar와 함께 배포되는 것을 전재로 하며, 실제 API 로써의 동작 자체를 다루지는 않는다.
| 순위 | 원칙 | 구현 |
|---|---|---|
| 1 | 요청 수신·응답 반환 최우선 | 모든 에러는 RFC 7807 JSON 반환, 앱 중단 없음 |
| 2 | 로그로 상태 확인 용이 | Structured JSON logging, request ID, access log |
| 3 | 커넥션 풀 관리 | lifespan 기반 생성/종료, /status에서 조회 |
| 구성 | 상세 |
|---|---|
| Language | Python 3.13 |
| Framework | FastAPI 0.136+ / uvicorn 0.46+ (multi-worker) |
| Config | YAML (모든 값 string, 앱 내부 coercion) |
| Logging | Structured JSON, 크기(100M) + 일자 로테이션 |
| Container | python:3.13-slim → Harbor registry |
| Orchestration | K8s 1.35, native sidecar (healthz) |
| Monitoring | Prometheus metrics, HPA |
| Cache | cashews + diskcache (disk://, 워커 간 공유) |
| Process | setproctitle (pgrep 가시성) |
+--- Pod (namespace: apitest) -----------------------------------------+
| shareProcessNamespace: true |
| terminationGracePeriodSeconds: 60 |
| |
| +- init: render-config -------------------------------------------+ |
| | envsubst: Secret env -> ConfigMap template ${VAR} replace | |
| | result: tmpfs (emptyDir Memory) - no disk write | |
| | (terminated after execution) | |
| +-----------------------------------------------------------------+ |
| |
| +- init: healthz (native sidecar) -+ +- container: pseudo ------+ |
| | UID 1001, readOnlyRootFilesystem| | UID 0 | |
| | SYS_PTRACE capability | | | |
| | | | :8080 | |
| | :9001 /health (self probe) | | / (info) | |
| | :9000 /healthz (target probe) | | /health (status) | |
| | | | /health/workers | |
| | Checks: | | /health/detail | |
| | TCP -> 127.0.0.1:8080 | | /metrics (prom) | |
| | HTTP -> GET /health -> "pseudo"| | /status (pools) | |
| | Proc -> "pseudo" (contains) | | | |
| +----------------------------------+ | Workers: 2 (uvicorn) | |
| | pseudo: master | |
| kubelet probes: | pseudo: worker [pid=N] | |
| pseudo -> :9000/healthz (healthz) +--------------------------+ |
| healthz -> :9001/health (self) |
| |
| Prometheus -> :8080/metrics (scrape) |
+----------------------------------------------------------------------+
| PID | 프로세스 | UID | 역할 |
|---|---|---|---|
| 1 | /pause | 65535 | infra container |
| 7 | python -m app.main --config ... | 1001 | healthz sidecar |
| 13 | pseudo: master | 0 | uvicorn master (워커 관리) |
| 19 | resource_tracker | 0 | multiprocessing 리소스 추적 |
| 20 | pseudo: worker [pid=20] | 0 | uvicorn worker 1 |
| 21 | pseudo: worker [pid=21] | 0 | uvicorn worker 2 |
setproctitle 적용으로 pgrep -f "pseudo: worker" 검색 가능.
+-----------------+
| Prometheus |
| (monitoring ns) |
+----+------------+
| scrape :8080/metrics
| (app_workers_idle,
| app_http_requests_total, ...)
|
+------------+ +----v--------------------------------------+
| kubelet | | pseudo Pod |
| | probe | |
| liveness --+-:9000--->| healthz --tcp/http/proc--> pseudo |
| readiness -+ | |
| startup ---+ | pseudo:8080 |
| | +- /health (healthz check) |
| | +- /health/workers (HPA ref) |
| | +- /health/detail (diagnostics) |
| | +- /metrics (Prometheus) |
| | +- /status/pools/* (pool status) |
+------------+ +--------------------------------------------+
cloudnative/app_works/pseudo/
├── Dockerfile # python:3.13-slim -> /opt/pseudo/
├── manifests/
│ ├── 00_namespace.yaml # namespace (__NAMESPACE__ token)
│ ├── 01_configmap.yaml # common + resources(template) + healthz
│ ├── 02_deployment.yaml # envsubst init + healthz sidecar + pseudo
│ ├── 02_secret.yaml # datasource secrets (stringData)
│ ├── 03_service.yaml # ClusterIP :8080
│ ├── 04_hpa.yaml # CPU/Memory + workers_idle
│ ├── common_config.yaml # common config (annotated, reference)
│ ├── resources_config.yaml # resource config v1 (annotated)
│ └── resources_config_v2.yaml # resource config v2 (reference)
├── pseudo/ # -> container /opt/pseudo/
│ ├── requirements.txt
│ └── app/
│ ├── __init__.py # __version__ = "0.1.0"
│ ├── __main__.py # python -m app.main entry
│ ├── main.py # app factory + uvicorn run
│ ├── config/ # config load/validate/coerce
│ │ ├── errors.py # ConfigFatalError / ConfigWarning
│ │ ├── coercion.py # string -> int/port/enum/host/timeout
│ │ ├── schema.py # CommonConfig / ResourcesConfig
│ │ └── loader.py # YAML parse + mutual exclusion check
│ ├── logging/ # structured logging
│ │ ├── config.py # console + file handler setup
│ │ └── rotation.py # size(100M) + daily rotation
│ ├── exceptions/ # error handling
│ │ ├── errors.py # AppError hierarchy
│ │ └── handlers.py # RFC 7807 global handler
│ ├── resources/ # connection pool
│ │ └── pool_manager.py # httpx pool create/get/close
│ ├── middleware/ # middleware
│ │ ├── request_id.py # X-Request-ID propagation
│ │ ├── access_log.py # structured access log
│ │ └── metrics_collector.py # request count/latency/status
│ └── routes/ # endpoints
│ ├── root.py # GET /
│ ├── health.py # /health, /health/workers, /health/detail
│ ├── metrics.py # /metrics (Prometheus)
│ └── status.py # /status, /status/pools/{type}/{id}
└── scripts/
├── build.sh # docker build + push to Harbor
└── deploy.sh # kubectl apply with __NAMESPACE__
앱 공통 설정. 모든 값은 string으로 작성.
| 필드 | 기본값 | 타입변환 | 범위 | 필수 |
|---|---|---|---|---|
configs.workers |
2 | str->int | 1~64 (clamp) | O |
configs.addresses.ipv4 |
0.0.0.0 | str | IP | - |
configs.addresses.port |
8080 | str->int | 1~65535 | - |
configs.addresses.ipv6 |
: : * | str | - | - |
configs.log.loglevel |
WARNING | str(enum) | DEBUG~CRITICAL | O |
configs.log.logdir |
/opt/pseudo/log | str(path) | 절대경로 | O |
configs.log.logfile |
app.log | str | - | O |
configs.log.logsize |
100M | str->bytes | 1M~10G | O |
configs.log.consolelevel |
INFO | str(enum) | DEBUG~CRITICAL | O |
system.alertgateway.host |
- | str | - | 섹션 있으면 필수 |
system.alertgateway.port |
- | str->int | 1~65535 | 섹션 있으면 필수 |
변환 정책:
"3.9" -> 3데이터소스 커넥션 풀 설정. 구조: resources.{타입}.{식별자}.conn/auth
resources:
postgresql: # 타입명
main_db: # 식별자
conn:
host: "db.internal" # scheme 미지정 시 http:// 자동 추가
port: "5432" # 필수, str->int
timeout: "300" # 선택, 기본 60초, str->int
servicename: "orcl" # Oracle SID/서비스명
database: "mydb" # DB/스키마명
topic: "events" # MQ 토픽명
auth: # 선택, 있으면 user/password 모두 필수
user: "${MAIN_DB_USER}"
password: "${MAIN_DB_PASSWORD}"
상호 배타 키 (동시 존재 시 exit 1):
servicename vs siddatabase vs schemaconn 필드 상세:
| 필드 | 기본값 | 필수 | 설명 |
|---|---|---|---|
| host | - | O | scheme 없으면 http:// 추가 |
| port | - | O | 1~65535 |
| timeout | 60 | - | 초 단위, 1~86400 |
| servicename | null | - | Oracle: SID/서비스명. 그 외: schema명 |
| sid | null | - | servicename 별칭 (동시 사용 불가) |
| database | null | - | DB명. Oracle에서 null이면 에러 |
| schema | null | - | database 별칭 (동시 사용 불가) |
| topic | null | - | MQ 토픽명. 미지원 시스템이면 무시 |
구조: resources.{식별자}.type + conn/auth (식별자가 최상위)
resources:
main_db: # 식별자가 최상위 키
type: "postgresql" # 타입은 필드
conn: ...
auth: ...
v2는 코드에 미적용 (참조용). 적용 시 loader 수정 필요.
| 변수 | 기본값 | 용도 |
|---|---|---|
APPLICATION_NAME |
pseudo | 프로세스 타이틀, /health app 필드, FastAPI title |
PSEUDO_COMMON_CONFIG |
/opt/pseudo/config/common_config.yaml | 공통 설정 경로 |
PSEUDO_RESOURCES_CONFIG |
/opt/pseudo/config/resources_config.yaml | 리소스 설정 경로 |
POD_NAME |
- | /health/detail system 정보 |
HOSTNAME |
- | /health/detail system 정보 |
{
"app": "pseudo",
"version": "0.1.0",
"status": "running"
}
healthz sidecar의 HTTP 체크 대상. datasource 연결 상태 포함.
| status | 조건 | datasources |
|---|---|---|
OK |
모든 datasource 정상 | "all connected" |
UP |
일부 datasource 실패 | "main_db, oracle_1" (실패 식별자 목록) |
OK |
풀 미설정 | "none configured" |
{
"status": "OK",
"app": "pseudo",
"datasources": "all connected",
"timestamp": "2026-05-14T07:22:17.410Z"
}
/proc 스캔으로 APPLICATION_NAME: worker 프로세스를 감지.
{
"status": "UP",
"timestamp": "2026-05-14T07:18:14.424Z",
"workers": {
"configured": 2,
"active": 2,
"idle": 1,
"pid_list": [27, 28]
}
}
| 필드 | 설명 |
|---|---|
| configured | common_config의 workers 값 |
| active | /proc에서 감지된 워커 프로세스 수 |
| idle | active - busy (sleeping 상태 워커) |
| pid_list | 워커 PID 목록 |
워커 + 각 데이터소스 HEAD 요청 연결 테스트 + 시스템 정보.
{
"status": "DEGRADED",
"timestamp": "2026-05-14T07:18:14.497Z",
"app": {
"name": "pseudo",
"version": "0.1.0",
"uptime_seconds": 255.6,
"pid": 27
},
"workers": {
"configured": 2,
"active": 2,
"idle": 1,
"pid_list": [27, 28]
},
"datasources": [
{
"pool": "postgresql/main_db",
"type": "postgresql",
"identifier": "main_db",
"status": "UP",
"latency_ms": 12.3,
"http_status": 200,
"meta": {}
},
{
"pool": "oracle/oracle_1",
"type": "oracle",
"identifier": "oracle_1",
"status": "DOWN",
"latency_ms": -1,
"meta": {"servicename": "orcl"},
"error": "ConnectError: [Errno -2] Name or service not known"
}
],
"system": {
"hostname": "pseudo-69f444849c-bwk8c",
"pod_name": "pseudo-69f444849c-bwk8c"
}
}
status 판정:
UP: 워커 존재 + 모든 datasource UPDEGRADED: 워커 없음 또는 datasource 1개 이상 DOWN# HELP app_workers_configured Number of configured workers
# TYPE app_workers_configured gauge
app_workers_configured 2
# HELP app_workers_active Number of active worker processes
# TYPE app_workers_active gauge
app_workers_active 2
# HELP app_workers_idle Number of idle worker processes
# TYPE app_workers_idle gauge
app_workers_idle 1
# HELP app_http_requests_total Total HTTP requests
# TYPE app_http_requests_total counter
app_http_requests_total 142
# HELP app_http_errors_total Total 5xx errors
# TYPE app_http_errors_total counter
app_http_errors_total 0
# HELP app_http_avg_latency_ms Average response latency in ms
# TYPE app_http_avg_latency_ms gauge
app_http_avg_latency_ms 6.26
# HELP app_http_requests_by_status Requests by HTTP status code
# TYPE app_http_requests_by_status counter
app_http_requests_by_status{code="200"} 140
app_http_requests_by_status{code="404"} 2
# HELP app_http_requests_by_method Requests by HTTP method
# TYPE app_http_requests_by_method counter
app_http_requests_by_method{method="GET"} 142
# HELP app_cache_hits Cache hit count
# TYPE app_cache_hits counter
app_cache_hits 24
# HELP app_cache_misses Cache miss count
# TYPE app_cache_misses counter
app_cache_misses 12
# HELP app_cache_hit_ratio Cache hit ratio percent
# TYPE app_cache_hit_ratio gauge
app_cache_hit_ratio 66.7
# HELP app_cache_keys Number of cached keys
# TYPE app_cache_keys gauge
app_cache_keys 2
{
"status": "ok",
"pools": ["postgresql/main_db", "oracle/oracle_1", "kafka/event_bus"]
}
{
"pool": "postgresql/main_db",
"status": "available",
"meta": {"database": "mydb"}
}
OpenAPI 기반 API 문서. 브라우저에서 http://<host>:8080/docs 접속.
| 항목 | 값 |
|---|---|
| 라이브러리 | cashews 7.x + diskcache 5.x |
| 백엔드 | disk:// (파일 기반, mmap) |
| 워커 간 공유 | O (파일 기반이므로 프로세스 간 공유) |
| Redis 필요 | X |
| 캐시 디렉토리 | /opt/pseudo/cache/ |
| 초기화 | lifespan startup |
| 종료 | lifespan shutdown (cache.close()) |
| 키 | 대상 | TTL |
|---|---|---|
health:ds_failed |
/health datasource 체크 결과 | 30초 |
health:ds_detail |
/health/detail datasource 상세 | 15초 |
| 메트릭 | 타입 | 설명 |
|---|---|---|
app_cache_hits |
counter | 캐시 히트 수 |
app_cache_misses |
counter | 캐시 미스 수 |
app_cache_hit_ratio |
gauge | 히트 비율 (%) |
app_cache_keys |
gauge | 저장된 키 수 |
Exception
└─ AppError (status_code, error_type, detail)
├─ ConfigurationError -> 500, "config-error"
├─ ResourceUnavailableError -> 503, "resource-unavailable"
└─ ValidationError -> 422, "validation-error"
모든 에러는 application/problem+json으로 반환:
{
"type": "resource-unavailable",
"title": "ResourceUnavailableError",
"status": 503,
"detail": "kafka broker unreachable",
"instance": "http://localhost:8080/api/process"
}
| 핸들러 | 대상 | 동작 |
|---|---|---|
| AppError handler | AppError 하위 | WARN 로그 + RFC 7807 응답 |
| ValidationError handler | FastAPI RequestValidationError | WARN 로그 + 422 |
| Unhandled handler | 그 외 모든 Exception | ERROR 로그 (exc_info) + 500 |
| 대상 | 형식 | 레벨 | 설정 |
|---|---|---|---|
| Console (stdout) | YYYY-MM-DDThh:mm:ss [LEVEL] logger: message |
consolelevel (기본 INFO) | config |
| File | JSON {"timestamp","level","logger","message"} |
loglevel (기본 WARNING) | config |
| 트리거 | 결과 파일 | 예시 |
|---|---|---|
| 크기 초과 (logsize, 기본 100M) | app_N.log (N: 0~9 순환) |
app_0.log ~ app_9.log |
| 날짜 변경 (자정) | app_YYYYMMDD.log |
app_20260514.log |
{logfile} (기본 app.log)Access Log (모든 요청):
{client_ip} {method} {path} - {status_code} {elapsed}s rid={request_id}
Request ID:
X-Request-ID 있으면 전파X-Request-ID에 반환02_secret.yaml 01_configmap.yaml init: render-config
(stringData) (${VAR} placeholder) (envsubst)
|
.datasources_secret --mount--> /opt/pseudo/conf/templates/ |
export KEY=VALUE .datasources_secret |
| |
. (source) |
| |
env loaded -----> envsubst |
| |
${MAIN_DB_PASSWORD} |
| replace |
P@s5w0rD!main |
| |
tmpfs (Memory) |
| |
v |
/opt/pseudo/config/ |
resources_config.yaml |
pseudo container <-- read ---------------- (plaintext, gone on |
Pod deletion) |
{식별자}_{필드} (대문자, 언더스코어 구분)
| Secret key | 용도 |
|---|---|
| MAIN_DB_USER | postgresql/main_db 접속 계정 |
| MAIN_DB_PASSWORD | postgresql/main_db 비밀번호 |
| ORACLE_1_USER | oracle/oracle_1 접속 계정 |
| ORACLE_1_PASSWORD | oracle/oracle_1 비밀번호 |
| EVENT_BUS_USER | kafka/event_bus 접속 계정 |
| EVENT_BUS_PASSWORD | kafka/event_bus 비밀번호 |
/opt/pseudo/conf/templates/.datasources_secret. (source)로 환경변수 로드 -> envsubst 실행 -> 종료emptyDir(medium: Memory) = tmpfs, 디스크 미기록cat config 시 평문 노출 -> RBAC exec 통제 필요| Check | 설정 | 대상 | 판정 |
|---|---|---|---|
| TCP | host: 127.0.0.1, ports: [8080] | 포트 소켓 연결 | PASS: 연결 성공 |
| HTTP | GET /health, expected_body: "pseudo" | HTTP 응답 + body match | PASS: 200 + "pseudo" 포함 |
| Process | match_mode: contains, names: ["pseudo"] | /proc cmdline 스캔 | PASS: 1개 이상 매칭 |
| Probe | 컨테이너 | 포트 | 경로 | 주기 |
|---|---|---|---|---|
| pseudo startup | healthz | 9000 | /healthz | 5s, 최대 12회(60s) |
| pseudo liveness | healthz | 9000 | /healthz | 30s |
| pseudo readiness | healthz | 9000 | /healthz | 10s |
| healthz liveness | healthz | 9001 | /health | 30s |
| healthz readiness | healthz | 9001 | /health | 5s |
healthz(UID 1001) != pseudo(UID 0):
/proc/<pid>/environ 접근 불가 -> 환경변수 수집 실패/proc/<pid>/root 접근 불가 -> language_runtimes 수집 실패| 메트릭 | 소스 | 임계치 | 역할 | 장애 시 |
|---|---|---|---|---|
| cpu | metrics-server | 80% | 기본 | 항상 동작 |
| memory | metrics-server | 80% | 기본 | 항상 동작 |
| app_workers_idle | Prometheus Adapter | < 2 | 보조 | <unknown> -> 무시, fallback |
cpu -> 3 replicas required
memory -> 2 replicas required
workers -> 5 replicas required (or <unknown> -> ignored)
────────────────────────────────
result -> max(3, 2, 5) = 5 replicas (or max(3, 2) = 3)
| 방향 | 안정화 | 속도 |
|---|---|---|
| Scale-Up | 15초 | 60초당 최대 4파드 또는 100% |
| Scale-Down | 300초 (5분) | 60초당 1파드 |
<unknown> -> K8s가 무시# 빌드 서버에서 실행 (haedong@192.168.2.1)
cd ~/data/workbench/cloudnative/app_works/pseudo
./scripts/build.sh latest
이미지: harbor.nova.office/library/app_works/pseudo:latest
./scripts/deploy.sh apply apitest
생성되는 리소스:
# Pod 상태
kubectl -n apitest get pods -l app=pseudo # 2/2 Running 확인
# /health
kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8080/health').read().decode())"
# /health/workers
kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8080/health/workers').read().decode())"
# /metrics
kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8080/metrics').read().decode())"
# healthz target check
kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:9000/healthz').read().decode())"
# 프로세스 확인 (setproctitle)
kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python3 -c "
import pathlib
for p in sorted(pathlib.Path('/proc').iterdir()):
if not p.name.isdigit(): continue
try:
cmd = (p/'cmdline').read_text().replace(chr(0),' ').strip()
if cmd: print(f'PID {p.name:>6} {cmd[:100]}')
except: pass
"
# HPA 상태
kubectl -n apitest get hpa pseudo
./scripts/deploy.sh delete apitest
# 로그 확인
kubectl -n apitest logs deploy/pseudo -c pseudo --tail=50
kubectl -n apitest logs deploy/pseudo -c render-config
일반적인 원인:
[CRITICAL] mandatory field ... is invalid -> 설정 필수 필드 누락/잘못된 형식YAML parse error -> YAML 문법 오류servicename and sid cannot both exist -> 상호 배타 키 중복kubectl -n apitest exec deploy/pseudo -c pseudo -- \
python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:9000/healthz').read().decode())"
expected body 'X' not found -> healthz config의 expected_body가 /health 응답에 없음connection refused -> pseudo 앱이 아직 시작 안 됨 (startup probe 대기)kubectl -n apitest get hpa pseudo
<unknown>/2 -> Prometheus Adapter 미설치. CPU/Memory만으로 동작하면 정상/health/detail에서 datasource status DOWN:
Name or service not known -> DNS 미해석. 호스트명/서비스 확인Connection refused -> 대상 서비스 미가동 또는 포트 불일치timeout -> 네트워크 도달 불가 또는 대상 서비스 과부하| 항목 | 결과 |
|---|---|
| Pod | 2/2 Running (pseudo + healthz) |
| GET /health | {"status":"UP","app":"pseudo","datasources":"main_db, oracle_1, event_bus"} |
| GET /health/workers | configured=2, active=1, idle=1, pid=[27] |
| GET /health/detail | DEGRADED (샘플 datasource 미존재 — 정상 동작) |
| GET /metrics | Prometheus format, 8 metrics 노출 |
| healthz TCP | PASS (8080 소켓 정상) |
| healthz HTTP | PASS (200, "pseudo" body match) |
| healthz Process | PASS (pseudo 프로세스 3개 매칭) |
| setproctitle | pseudo: master, pseudo: worker [pid=N] |
| Simulation | 10/10 passed |
| Pretty JSON | 모든 응답 indent=2 적용 |