본문으로 건너뛰기

Helm으로 Kubernetes에 설치

대상 독자

프로덕션 등급 Helm 차트로 TRUSCA를 배포하려는 Kubernetes 운영자. kubectl, Helm 3, 기본 클러스터 관리(Ingress, StorageClass, cert-manager) 숙련도를 전제합니다. 단일 호스트를 운영한다면 Docker Compose 설치가 더 간단합니다.

Helm 차트(charts/trustedoss, 차트 버전 0.22.6)는 포털 전체를 배포합니다. FastAPI 백엔드, Celery 워커와 beat 스케줄러, React 프론트엔드, TLS가 적용된 Ingress, 데이터베이스 마이그레이션 Job을 포함합니다. PostgreSQL과 Redis는 클러스터 내부에 번들(평가용)하거나 외부 관리형 데이터스토어를 가리킬 수 있습니다(프로덕션 권장).

레지스트리가 아니라 체크아웃에서 설치합니다

차트는 아직 레지스트리에 발행하지 않았습니다. 그래서 oci:// 설치 경로가 없습니다. 저장소를 클론한 뒤 작업 트리에서 차트를 설치하십시오. 아래 명령이 그렇게 되어 있습니다.

차트의 appVersion은 이제 포털 릴리스를 따라갑니다. 따라서 기본 설치에서도 image.tag를 따로 지정하지 않아도 현재 이미지를 받습니다. 레지스트리 발행에는 chart-vX.Y.Z 태그와 패키지 공개 설정이 한 번 필요하고, 그것이 이슈 #81의 남은 절반입니다.

취약점 매칭은 차트 내장

워커 파드는 Trivy DB를 포함하며 ghcr.io/aquasecurity/trivy-db에서(또는 env.trivy.dbRepository 미러에서) 다운로드·갱신합니다. 외부 취약점 엔진은 필요하지 않습니다. 취약점 데이터 (Trivy DB) 참조.

차트가 배포하는 것

워크로드종류비고
backendDeploymentFastAPI API. AUTO_MIGRATE=false이며 마이그레이션은 Job이 수행합니다.
workerDeployment (+ 선택적 HPA)Celery 워커 (cdxgen / scancode / Trivy).
beatDeployment (replicas: 1)Celery 스케줄러. 싱글턴입니다.
frontendDeploymentnginx 위 React SPA (:8080).
postgresStatefulSet선택적 번들 (postgres.bundled).
redisDeployment선택적 번들 (redis.bundled).
migrateJob (pre-install / pre-upgrade 훅)owner 역할로 alembic upgrade head.
ingressIngresscert-manager TLS; API + SPA 라우팅.

사전 요구사항

  • 네임스페이스와 워크로드를 생성할 권한이 있는 Kubernetes 클러스터와 kubectl 컨텍스트.
  • Helm 3.
  • 인그레스 컨트롤러(차트 기본 클래스는 nginx).
  • 기본 TLS 구성을 위한 letsencrypt-prod 이름의 ClusterIssuer가 있는 cert-manager(ingress.annotations로 재정의 가능).
  • 다중 노드 클러스터에서는 공유 스캔 워크스페이스용 ReadWriteMany StorageClass(workspace.persistence.storageClassName). 단일 노드 클러스터는 파드별 emptyDir 폴백을 사용할 수 있습니다.

설치 전 차트 검증

배포 전에, 클러스터 없이 in-repo 차트를 로컬에서 렌더링하여 values·템플릿 오류를 잡습니다(Helm 3+, 저장소 루트에서 실행):

SECRET=$(openssl rand -hex 32)
HMAC_SECRET=$(openssl rand -hex 32)
helm lint charts/trustedoss \
--set env.secret.secretKey="$SECRET" \
--set env.secret.apiKeyHmacSecret="$HMAC_SECRET" \
--set postgres.auth.password=throwaway \
--set ingress.host=trustedoss.example.com
helm template trustedoss charts/trustedoss --namespace trustedoss \
--set env.secret.secretKey="$SECRET" \
--set env.secret.apiKeyHmacSecret="$HMAC_SECRET" \
--set postgres.auth.password=throwaway \
--set ingress.host=trustedoss.example.com \
>/dev/null

helm lint는 차트 구조 문제를 보고하고, helm template은 최소 필수 values로 모든 매니페스트를 완전히 렌더링하므로 0이 아닌 종료 코드는 차트가 설치되지 않음을 뜻합니다. 여기 --set 값은 일회용이며, 실제 설치는 아래에서 본인의 시크릿을 사용합니다.

빠른 시작 (번들 데이터스토어, 평가용)

PostgreSQL과 Redis를 클러스터 내부에서 실행합니다. 빠르게 띄울 수 있지만 프로덕션 데이터에는 권장하지 않습니다.

git clone https://github.com/trustedoss/trusca.git && cd trusca
helm install trustedoss ./charts/trustedoss \
--namespace trustedoss --create-namespace \
--set env.secret.secretKey="$(openssl rand -hex 32)" \
--set env.secret.apiKeyHmacSecret="$(openssl rand -hex 32)" \
--set postgres.auth.password="$(openssl rand -hex 24)" \
--set ingress.host=trustedoss.example.com \
--set env.corsAllowedOrigins=https://trustedoss.example.com

trustedoss.example.com을 자신의 호스트명으로 바꾸고, 해당 호스트의 DNS가 인그레스 컨트롤러를 가리키는지 확인하십시오.

번들 데이터스토어는 평가용입니다

클러스터 내부 PostgreSQL과 Redis는 기본값이 소박하고 단일 레플리카입니다. 시험 이상의 용도라면 외부 관리형 데이터스토어(아래)를 사용하십시오.

프로덕션(외부 관리형 데이터스토어 권장)

클러스터 내부 번들 대신 PostgreSQL은 Cloud SQL / RDS, Redis는 Memorystore / ElastiCache를 권장합니다. values 파일을 제공하십시오.

# values.prod.yaml
postgres:
bundled: false
redis:
bundled: false
env:
database:
url: postgresql+asyncpg://app:***@cloudsql-proxy:5432/trustedoss
# DDL/owner 역할을 런타임 역할과 분리하는 경우:
ownerUrl: postgresql+asyncpg://owner:***@cloudsql-proxy:5432/trustedoss
redis:
url: redis://memorystore:6379/0
secret:
# 다섯 개 키를 모두 담은 사전 생성 Secret (아래 참고)
existingSecret: trustedoss-prod-secrets
corsAllowedOrigins: https://trustedoss.example.com
ingress:
host: trustedoss.example.com

기동 시 데이터베이스 역할 확인

백엔드는 기동할 때 PostgreSQL에 지금 접속한 역할이 무엇을 할 수 있는지 묻고 그 결과를 기록합니다.

  • db.role.separation.active: 런타임이 행을 읽고 쓰는 것까지만 할 수 있습니다. env.database.ownerUrl을 따로 두는 이유가 이것입니다. 백엔드가 장악되더라도 감사 트리거를 지우거나 테이블을 바꿀 수 없습니다.
  • db.role.separation.missing: 런타임은 DDL까지 수행할 수 있습니다. env.database.url이 소유 역할을 가리키면 이렇게 나오며, 이것도 지원하는 단일 역할 배포입니다. 분리하고 싶다면 무엇을 바꿔야 하는지 메시지에 담겨 있습니다.

이 확인은 역할 이름이 아니라 권한을 묻습니다. 그래서 DML 전용 역할의 이름이 trustedoss_app이 아닌 데이터베이스도 올바르게 판정됩니다.

두 번째 경우를 경고가 아니라 기동 실패로 다루려면 REQUIRE_DB_ROLE_SEPARATION=true를 설정합니다. 단일 역할도 지원하는 구성이라 기본값은 꺼짐이며, 분리가 필수인 곳에서만 켜십시오. 권한을 판정할 수 없으면 통과시키지 않고 막습니다.

예전 버전의 기동 거부

이전 버전은 DATABASE_URL_APP이 설정돼 있는데 접속한 역할이 trustedoss_app이 아니면 무조건 기동을 거부했습니다. 차트는 그 키를 언제나 쓰기 때문에, values.yaml이 권하는 구성인 단일 역할 외부 데이터베이스가 뜨지 못했습니다. 게다가 오류 메시지는 쿠버네티스에는 있지도 않은 docker-compose 배선을 확인하라고 안내했습니다.

설치합니다.

helm install trustedoss ./charts/trustedoss \
--namespace trustedoss --create-namespace \
-f values.prod.yaml
Secret 구성은 필수입니다

env.secret.existingSecret을 설정하면 차트는 자체 Secret을 렌더링하지 않습니다. 참조하는 Secret은 다섯 개 키를 모두 담아야 하며, 그렇지 않으면 파드가 시작되지 않습니다.

  • DATABASE_URL_APP
  • DATABASE_URL_OWNER
  • REDIS_URL
  • SECRET_KEY (최소 32자)
  • API_KEY_HMAC_SECRET (최소 32자, SECRET_KEY와 독립된 값. 두 값을 같게 두지 마십시오)

existingSecret을 설정하지 않으면 env.secret.secretKeyenv.secret.apiKeyHmacSecret 둘 다 차트 자체가 요구하는 필수 입력값입니다. 값이 비어 있으면 API_KEY_HMAC_SECRETsecretKey에서 유도하는 대신 릴리스 렌더링 자체가 실패합니다. 각각 openssl rand -hex 32로 독립적으로 생성하십시오.

프로덕션 CORS

env.corsAllowedOrigins는 SPA를 제공하는 정확한 오리진을 열거해야 합니다. 프로덕션에서 와일드카드는 금지합니다. 브라우저가 사용할 모든 scheme + host를 나열하십시오.

관리형 클라우드 데이터스토어 없이 고가용성 구성하기

위 예시는 관리형 Postgres/Redis(Cloud SQL, RDS, Memorystore, ElastiCache)를 전제합니다. 대신 자체 클러스터에 셀프호스팅한다면, 이 차트가 번들로 제공하는 Postgres(단일 Pod, 복제 없음)와 Redis(단일 Pod, Sentinel/Cluster 없음)는 그 자체로 고가용성 구성이 아닙니다. 위 "빠른 시작" 절과 마찬가지로 평가·소규모 설치용입니다. 이 차트는 Postgres/Redis 클러스터링 자체를 구현하지 않습니다. 리더 선출, WAL 스트리밍, 장애 조치, 백업 조율은 그 자체로 하나의 오퍼레이터가 맡을 몫이고, Helm 차트 안에 오케스트레이터를 다시 만드는 것은 이 프로젝트의 범위 밖입니다. 대신 각 데이터스토어를 이미 존재하는 전용 K8s 오퍼레이터 아래에서 돌리고, 관리형 클라우드 예시와 똑같이 bundled: false로 이 차트가 그 Service를 가리키게 하십시오.

  • Postgres: CloudNativePGZalando postgres-operator 모두 하나의 Service 이름 뒤에서 자동 장애 조치가 되는 primary + replica 구성을 제공합니다. env.database.url(권한 분리를 쓴다면 env.database.ownerUrl도)을 Cloud SQL/RDS 대신 그 Service로 가리키면 나머지 values 파일은 그대로입니다.
  • Redis: Sentinel이나 Cluster 모드 배포(예: sentinel.enabled: true를 쓴 Bitnami redis 차트나 Redis Cluster)는 클라이언트에게 보이는 주소 하나 뒤에서 장애 조치를 제공합니다. env.redis.url을 같은 방식으로 그곳에 가리키십시오.

어느 쪽이든 postgres.bundled: false / redis.bundled: false를 설정해 이 차트가 자체 데이터스토어 오브젝트를 렌더링하지 않고 오퍼레이터가 관리하는 쪽에 전적으로 맡기게 하십시오.

공유하는 Redis 인스턴스 자체가 단일 장애 지점 아닌가?

여기서 Redis는 한 번에 네 가지 역할을 맡습니다. Celery 브로커, Celery 결과 백엔드, 요청 rate limiter, WebSocket 연결 레지스트리(core/ws_registry.py)가 모두 하나의 env.redis.url을 거칩니다. 이걸 여러 Redis 인스턴스·인덱스로 나눌 가치가 있는지는 장애 상황에서 각 역할이 실제로 어떻게 되는지에 달려 있으므로, 추측 대신 현재 사실을 그대로 적습니다.

  • rate limiter와 로그인 스로틀은 이미 Redis 오류에서 fail open 합니다 (core/ratelimit.py, core/login_throttle.py, core/redis_degradation.py). Redis에 닿지 못한 요청은 거부되지 않고 통과되며, 이 완화 상태는 로그로 남고(중복 제거) /health/ready에도 노출됩니다. 이 영역의 장애는 rate limiting을 잃을 뿐 가용성을 잃지 않습니다.
  • Celery(스캔·알림·백업 태스크)는 브로커가 실제로 필요합니다. 장애가 나면 Redis가 복구될 때까지 태스크 디스패치가 멈춥니다. 이는 Redis 인스턴스를 둘로 나눠도 달라지지 않습니다. 브로커의 역할 자체가 Redis이기 때문입니다 (이 차트는 다른 브로커를 지원하지 않습니다).
  • WebSocket 연결 레지스트리(core/ws_registry.py)도 fail open 합니다(이슈 #458). 새 연결은 거부되지 않고 받아들여지므로, 실시간 스캔 진행률 스트리밍도 여기서 Redis를 쓰는 다른 것들과 마찬가지로 장애 동안 계속 동작합니다. 다만 이쪽의 트레이드오프는 더 좁습니다: 사용자당/전체 연결 수 상한이 장애가 지속되는 동안 적용되지 않아, Redis에 닿지 않는 동안은 클라이언트가 무제한으로 소켓을 열 수 있습니다. 이건 의도적으로 받아들인 선택입니다 (자연히 복구되고 운영자에게 보이는 장애 쪽을, 같은 기간 동안 모든 사용자의 실시간 기능이 완전히 막히는 쪽보다 우선함), rate limiter의 방식을 그대로 옮겨온 게 아닙니다. 전체 근거는 해당 모듈 자체의 docstring을 참고합니다.

이런 사실을 바탕으로, 이 세 가지 역할을 위해 Redis를 별도 인스턴스로 나누는 일은 지금 이 차트가 떠안지 않습니다. 그건 새로운 인프라 표면 (프로비저닝·모니터링·장애 조치까지 챙겨야 할 두 번째 데이터스토어)이고, 그 대가로 얻는 것은 일시적으로 남용 방지 장치 하나가 풀리는 정도이지 데이터 유실이나 파이프라인 정지, 기능 자체가 멈추는 문제가 아닙니다.

마이그레이션 동작 방식

Helm pre-install + pre-upgrade 훅 Job이 owner DB 역할 (DATABASE_URL_OWNER)로 alembic upgrade head한 번 실행합니다. 애플리케이션 파드는 AUTO_MIGRATE=false로 실행되므로 Job이 유일한 마이그레이터입니다.

백엔드 파드는 스키마가 HEAD에 도달할 때까지 NotReady(/health/ready503 반환)로 유지되므로, 트래픽은 마이그레이션된 스키마에만 도달합니다. 마이그레이션은 forward-only이며 Job은 절대 다운그레이드하지 않습니다. 번들 케이스의 훅 순서는 Secrets → Postgres Service / StatefulSet → 마이그레이션 Job이며, Job의 init 컨테이너는 alembic 실행 전 Postgres가 연결을 받을 때까지 대기합니다.

업그레이드

git -C trusca pull && git -C trusca checkout <새-태그>
helm upgrade trustedoss ./trusca/charts/trustedoss \
--namespace trustedoss \
-f values.prod.yaml

pre-upgrade 마이그레이션 Job이 새 파드 롤아웃 전에 새 스키마를 적용합니다. 마이그레이션은 forward-only이므로 업그레이드 전에 데이터베이스를 백업하십시오. 백업 및 복원을 참고하십시오.

주요 values

전체 표는 차트 README에 있습니다. 가장 자주 설정하는 값은 다음과 같습니다.

기본값용도
image.tag0.22.6backend / worker / frontend 이미지 태그(절대 :latest 금지).
ingress.host""필수. 공개 호스트명.
env.corsAllowedOrigins""프로덕션 필수. 허용 브라우저 오리진(와일드카드 금지).
env.secret.secretKey""SECRET_KEY(≥32자). existingSecret이 없으면 필수.
env.secret.apiKeyHmacSecret""API_KEY_HMAC_SECRET(≥32자), 저장된 API 키 비밀을 해시하는 전용 키. existingSecret이 없으면 필수이며, secretKey와 같은 값을 쓰면 안 됩니다.
env.secret.existingSecret""다섯 개 키를 담은 사전 생성 Secret; 차트 Secret을 비활성화.
postgres.bundledtruefalseenv.database.*(외부) 사용.
redis.bundledtruefalseenv.redis.url(외부) 사용.
env.trivy.dbRepositoryghcr.io/aquasecurity/trivy-dbair-gapped 사내 미러로 오버라이드. Air-gapped 운영 참조.
env.trivy.dbRefreshHours168주간 Trivy DB refresh. 낮추면 신선도↑.
worker.trivyDbPersistence.enabledtrue/var/lib/trivy에 PVC 마운트해 워커 재시작마다 재다운로드 방지.
workspace.persistence.storageClassName""다중 노드 클러스터의 공유 스캔 볼륨용 RWX 클래스.
worker.replicaCount2파드별 concurrency보다 워커 파드 스케일링을 권장.
env.extraEnv{}차트가 이름으로 다루지 않는 런타임 변수. 아래 참고.
env.extraEnvFrom[]직접 만든 Secret을 붙이는 envFrom 항목. 아래 참고.
env.extraVolumes[]backend와 worker와 beat에 붙는 volumes 항목입니다. 사설 인증기관 인증서가 여기로 들어갑니다.
env.extraVolumeMounts[]같은 셋에 붙는 volumeMounts 항목입니다. 사설 인증기관을 봅니다.

차트가 이름으로 다루지 않는 설정

위의 키들은 대부분 하나씩 명시되어 있습니다. 차트가 무엇을 지원하는지 분명해지는 대신, 릴리스마다 포털보다 뒤처집니다. env.extraEnvenv.extraEnvFrom이 그 나머지를 담당하며, 백엔드·워커·beat 파드에 모두 전달됩니다. 어떤 변수가 있는지는 환경변수에 정리되어 있습니다.

비밀이 아닌 설정은 env.extraEnv에 넣습니다.

env:
extraEnv:
SCANOSS_ENABLED: "true"
KEV_REFRESH_ENABLED: "false" # air-gapped: CISA 피드에 접근할 수 없음
DISK_HARD_LIMIT_PCT: "98"
WEBHOOK_RATE_LIMIT: "240/minute"

자격증명은 직접 만든 Secret에 넣고 env.extraEnvFrom으로 참조합니다. extraEnv에 적은 값은 values 파일에 남고, SMTP 비밀번호나 OAuth 클라이언트 시크릿은 커밋하는 파일에 둘 것이 아닙니다.

kubectl create secret generic trustedoss-notifications \
--from-literal=SMTP_HOST=smtp.example.com \
--from-literal=SMTP_USER=portal@example.com \
--from-literal=SMTP_PASSWORD='...' \
--from-literal=SLACK_WEBHOOK_URL='https://hooks.slack.com/services/...'
env:
extraEnvFrom:
- secretRef:
name: trustedoss-notifications

Helm 설치에서 OAuth 로그인, SMTP·Slack·Teams 알림, 저장소에 포함된 코드 식별 서비스, Jira 연동에 닿는 방법이 이것입니다. 이전에는 Helm 설치에서 이 설정들을 아예 지정할 수 없었고, 그 격차를 메우는 것이 이번 변경입니다.

작동 확인

  1. 마이그레이션 Job이 완료되었는지 확인합니다.

    kubectl -n trustedoss get jobs
    # trustedoss migrate Job의 COMPLETIONS가 1/1 이어야 합니다.
  1. 모든 파드가 Running이고 백엔드 파드가 Ready인지 확인합니다.

    kubectl -n trustedoss get pods
    # 백엔드 파드 Ready = /health/ready가 200 반환(스키마 HEAD).
  1. 클러스터 내부에서 readiness 프로브가 통과하는지 확인합니다.

    kubectl -n trustedoss exec deploy/trustedoss-backend -- \
    curl -fsS http://localhost:8000/health/ready
    # → {"status":"ready","redis":"ok"}

    redis 필드는 관측용일 뿐이며 200 응답을 503으로 바꾸지 않습니다. "degraded"로 나오면 온콜 런북을 참고하십시오.

  1. Ingress에 주소와 유효한 인증서가 있는지 확인한 뒤 브라우저에서 https://<ingress.host>/를 열어 로그인합니다.

문제 해결

  • 백엔드 파드가 NotReady에서 멈춤. 스키마가 HEAD에 도달할 때까지 /health/ready503을 반환합니다. 마이그레이션 Job 로그를 확인하십시오.

    kubectl -n trustedoss logs job/trustedoss-migrate

    Job 실패는 보통 owner DSN(DATABASE_URL_OWNER)에 DDL 권한이 없거나 데이터베이스에 도달할 수 없다는 의미입니다.

  • 기존 Secret 사용 시 파드가 CreateContainerConfigError. 참조 Secret에 네 개 필수 키 중 하나가 없습니다. 확인하십시오.

    kubectl -n trustedoss get secret trustedoss-prod-secrets -o jsonpath='{.data}' | tr ',' '\n'
    # DATABASE_URL_APP, DATABASE_URL_OWNER, REDIS_URL, SECRET_KEY 가 있어야 합니다.
  • 다중 노드 클러스터에서 스캔 실패. 백엔드와 워커가 스캔 워크스페이스를 공유합니다. ReadWriteMany StorageClass가 없으면 워커가 백엔드의 쓰기 내용을 읽을 수 없습니다. workspace.persistence.storageClassName을 RWX 클래스(nfs / efs / filestore / longhorn)로 설정하십시오.

  • TLS 인증서가 발급되지 않음. 기본 어노테이션은 letsencrypt-prod 이름의 cert-manager ClusterIssuer를 기대합니다. Certificate를 점검하십시오.

    kubectl -n trustedoss describe certificate

차트 버그를 만나면 버그 신고 템플릿으로 이슈를 열어 주십시오.

함께 보기