> ## Documentation Index
> Fetch the complete documentation index at: https://docs.befailproof.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Kubernetes 배포 가이드

> AgentEye Kubernetes 배포 가이드 문서.

이 가이드는 전용 Kubernetes 클러스터에 AgentEye 전체 스택을 배포하는 방법을 안내합니다:

* **ClickHouse 24.8** -- 이벤트 및 평가 분석의 기준 저장소 (100Gi 퍼시스턴트 볼륨을 가진 StatefulSet). 필수 항목: 없으면 서버가 시작되지 않습니다.
* **PostgreSQL 16** -- 조직, API 키, 사용자, 대시보드, 저장된 쿼리, 인증을 위한 관계형/메타데이터 저장소 (50Gi 퍼시스턴트 볼륨을 가진 StatefulSet)
* **Redis 7.2** -- 선택적 공유 캐시 및 속도 제한 백엔드; 사용 불가 시 서버와 대시보드는 기능이 저하된 상태로 계속 동작합니다
* **AgentEye Server** -- 이벤트 수집, 분석, 키 관리를 위한 Rust API (2개 레플리카)
* **AgentEye Dashboard** -- Next.js 웹 UI (2개 레플리카)
* **AI 어시스턴트 (에이전트 서비스)** -- 포트 9100에서 제공되는 선택적 읽기 전용 대시보드 내 어시스턴트; LLM 엔드포인트가 구성되기 전까지 비활성 상태
* **Traefik (공용)** -- 수집기 트래픽용 인그레스 컨트롤러, mTLS 보호
* **Traefik (대시보드)** -- 대시보드용 인그레스 컨트롤러, VPN/IP 허용 목록 전용
* **cert-manager** -- TLS 인증서 및 mTLS CA
* **백업 CronJob** -- 매일 UTC 03:00에 PostgreSQL + ClickHouse 통합 덤프 실행
* **인증서 갱신 모니터** -- 클라이언트 인증서 만료 임박 시 알림 발송

**예상 소요 시간:** 최초 배포 기준 60\~90분.

Exosphere가 모든 과정을 대신 처리하는 관리형 배포 모델에 대해서는 [enterprise-docs/managed-deployment.md](/ko/agenteye/managed-deployment)를 참조하세요.

***

## 사전 요구 사항

시작하기 전에 각 확인 명령을 실행하세요. 모든 항목을 통과해야 합니다.

| 요구 사항                   | 최솟값                                  | 확인 명령                                                | 예상 결과                                                                       |
| ----------------------- | ------------------------------------ | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| Kubernetes 클러스터         | 1.27+                                | `kubectl version`                                    | Server Version >= v1.27                                                     |
| Kustomize (kubectl에 포함) | Kustomize v1.14+ (kubectl 1.27+에 포함) | `kubectl kustomize --help`                           | 사용법 텍스트 출력                                                                  |
| Helm                    | v3                                   | `helm version`                                       | `Version:"v3.x.x"`                                                          |
| cluster-admin RBAC      | --                                   | `kubectl auth can-i create namespaces`               | `yes`                                                                       |
| 기본 StorageClass         | --                                   | `kubectl get storageclass`                           | `(default)` 표시가 있는 항목 최소 1개                                                 |
| LoadBalancer 지원         | --                                   | 클라우드 환경에 따라 다름 (EKS, GKE, AKS 모두 기본 지원)              | --                                                                          |
| GitHub PAT              | --                                   | `echo $AGENTEYE_TOKEN`                               | 값이 존재해야 함 ([enterprise-docs/github-token.md](/ko/agenteye/github-token) 참조) |
| openssl                 | --                                   | `openssl version`                                    | OpenSSL 1.x 또는 3.x                                                          |
| 클라우드 스토리지 버킷            | --                                   | PostgreSQL + ClickHouse 백업용 (S3, GCS, 또는 Azure Blob) | --                                                                          |

**클러스터 사양:** 최소 3개 노드, 각 노드당 4 vCPU / 8 GB RAM. 전체 요구 사항은 [enterprise-docs/managed-deployment.md](/ko/agenteye/managed-deployment)를 참조하세요.

### 모든 항목 한 번에 확인

```bash theme={null}
echo "--- Prerequisites Check ---"
kubectl version --client -o yaml 2>/dev/null | grep -q gitVersion && echo "PASS: kubectl" || echo "FAIL: kubectl not found"
helm version --short 2>/dev/null | grep -q v3 && echo "PASS: helm v3" || echo "FAIL: helm v3 not found"
kubectl auth can-i create namespaces 2>/dev/null | grep -q yes && echo "PASS: cluster-admin" || echo "FAIL: no cluster-admin"
kubectl get storageclass 2>/dev/null | grep -q default && echo "PASS: default StorageClass" || echo "FAIL: no default StorageClass"
[ -n "$AGENTEYE_TOKEN" ] && echo "PASS: AGENTEYE_TOKEN set" || echo "FAIL: AGENTEYE_TOKEN not set"
openssl version >/dev/null 2>&1 && echo "PASS: openssl" || echo "FAIL: openssl not found"
echo "---"
```

### 배포 구조

**수집 엔드포인트**는 사용자가 제어하는 호스트명(예: `ingest.your-company.example`)으로 제공됩니다. cert-manager는 HTTP-01 방식으로 Let's Encrypt에 공개 신뢰 TLS 인증서를 요청하므로, 수집기는 고객별 CA 고정 없이 시스템 신뢰 저장소에서 서버 인증서를 검증합니다.

**대시보드 엔드포인트**도 동일한 방식으로 동작합니다. 사용자가 제어하는 두 번째 호스트명(예: `agenteye.your-company.example`)으로 제공되며, 대시보드 Traefik LoadBalancer를 가리킵니다. cert-manager는 해당 LoadBalancer를 통해 Let's Encrypt 인증서를 발급합니다. 브라우저는 경고 없이 신뢰된 인증서를 받습니다.

> **인증서 발급 및 갱신은 HTTP-01로 검증됩니다.** 따라서 두 LoadBalancer 모두 인터넷에서 포트 80으로 접근 가능해야 합니다. 대시보드 LoadBalancer에 IP 제한이 필요한 경우, 먼저 지원팀과 DNS-01 솔버를 조율하세요 — 그렇지 않으면 갱신이 자동으로 실패하여 인증서가 만료됩니다.

***

## 매니페스트 가져오기

```bash theme={null}
git clone https://x:${AGENTEYE_TOKEN}@github.com/agenteye-enterprise/releases.git
cd releases/deploy
```

**확인:**

```bash theme={null}
ls base/kustomization.yaml
```

예상 결과: 파일이 존재해야 합니다. 존재하지 않으면 클론에 실패한 것입니다 — `AGENTEYE_TOKEN`을 확인하세요.

**디렉터리 구조:**

```
deploy/
  base/           공유 Kustomize 기반 (모든 K8s 리소스)
  overlays/       클러스터별 오버라이드 (이미지 태그, 호스트명, 리소스)
  third-party/    Traefik, cert-manager, 및 (선택적) Robusta 상태 모니터링용 Helm 값
```

**base**에는 Phase 3.1에서 구성하는 두 공용 호스트명의 Let's Encrypt 인증서를 포함한 전체 배포에 필요한 모든 리소스가 포함되어 있습니다. **overlay**는 특정 환경(예: 사용자 정의 이미지 태그, 리소스 제한, 환경 변수 연결)에 맞게 base를 패치합니다. **third-party** 디렉터리에는 외부 인프라용 Helm 값 파일이 있습니다.

> **상태 모니터링 (선택 사항):** 서버의 readiness probe는 이미 Postgres + ClickHouse 상태를 반영하며, `third-party/robusta/`는 선택적으로 Kubernetes 네이티브 파드 오류 알림을 Slack에 전송하는 기능을 추가합니다. [enterprise-docs/health-monitoring.md](/ko/agenteye/health-monitoring)를 참조하세요.

***

## Phase 1 -- 서드파티 인프라 (\~30분)

### 1.1 cert-manager 설치

cert-manager는 HTTPS용 TLS 인증서와 mTLS 클라이언트 인증서에 사용되는 프라이빗 CA를 관리합니다.

```bash theme={null}
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace \
  --set crds.install=true
```

**확인:**

```bash theme={null}
kubectl get pods -n cert-manager
```

예상 결과: 3개 파드가 모두 `Running` 상태 — `cert-manager`, `cert-manager-cainjector`, `cert-manager-webhook`.

```bash theme={null}
kubectl get crds | grep cert-manager
```

예상 결과: 최소 `certificates.cert-manager.io`, `clusterissuers.cert-manager.io`, `issuers.cert-manager.io`가 표시되어야 합니다.

**실패 시:** 파드가 `CrashLoopBackOff` 상태이면 일반적으로 CRD가 설치되지 않은 것입니다. `--set crds.install=true`를 추가하여 다시 실행하세요. webhook 파드가 readiness에 실패하면 30초 기다렸다가 다시 확인하세요 — 시작하는 데 잠시 시간이 걸릴 수 있습니다.

***

### 1.2 Traefik 설치 -- 공용 수집 컨트롤러

이 Traefik 인스턴스는 **외부** LoadBalancer에서 수집기 트래픽을 처리합니다. TLS를 종료하고 수집 엔드포인트에서 mTLS(클라이언트 인증서 검증)를 강제합니다.

```bash theme={null}
helm repo add traefik https://traefik.github.io/charts
helm repo update

helm install traefik-public traefik/traefik \
  --namespace traefik-public --create-namespace \
  --version 39.0.8 \
  -f third-party/traefik/values-public.yaml
```

**확인:**

```bash theme={null}
kubectl get pods -n traefik-public
```

예상 결과: 1개 파드가 `Running` 상태.

```bash theme={null}
kubectl get ingressclass traefik-public
```

예상 결과: IngressClass가 존재해야 합니다 (기본 클래스가 아님).

**실패 시:** `kubectl describe pod -n traefik-public <pod-name>`으로 이미지 풀 오류나 리소스 제약을 확인하세요.

***

### 1.3 Traefik 설치 -- 대시보드 컨트롤러

이 Traefik 인스턴스는 전용 LoadBalancer에서 대시보드를 제공하며, IP 허용 목록으로 접근을 제한합니다.

> **이 인스턴스에는 두 가지 허용 목록 메커니즘이 제공됩니다.** 이 가이드는 이식성 있는 `service.loadBalancerSourceRanges` 필드로 접근을 제한하는 `values-dashboard.yaml`을 사용합니다. `service.beta.kubernetes.io/aws-load-balancer-source-ranges` 어노테이션을 선호하는 AWS 환경을 위해 `values-internal.yaml`도 함께 제공됩니다. 하나를 선택하여 일관되게 사용하세요. 아래 단계는 `values-dashboard.yaml`을 기준으로 설명합니다.

**설치 전에** `third-party/traefik/values-dashboard.yaml`을 편집하여 허용된 소스 IP를 설정하세요. `loadBalancerSourceRanges` 필드는 대시보드에 접근할 수 있는 IP를 제어합니다. 기본값은 `0.0.0.0/0`(모든 IP)으로 설정되어 있으므로, VPN, 사무실 또는 알려진 이그레스 IP로 제한하세요.

#### 단일 IP 허용

```yaml theme={null}
service:
  loadBalancerSourceRanges:
    - "<YOUR_VPN_IP>/32"
```

#### 여러 IP 허용

IP 또는 CIDR 블록마다 하나씩 항목을 추가하세요. `/32` 접미사는 단일 IPv4 주소를 의미하며, CIDR 블록(예: `/24`)은 범위를 의미합니다. 개별 IP와 범위를 자유롭게 혼합할 수 있습니다:

```yaml theme={null}
service:
  loadBalancerSourceRanges:
    - "203.0.113.10/32"       # 사무실 게이트웨이
    - "203.0.113.11/32"       # 예비 사무실 게이트웨이
    - "198.51.100.0/24"       # VPN 풀
    - "192.0.2.50/32"         # 온콜 엔지니어 자택 IP
```

목록 관리 시 유의 사항:

* 줄당 하나의 항목을 유지하고 각 IP의 소유자나 목적을 `#` 주석으로 추가하세요. 이 정보는 나중에 운영자가 해당 항목이 아직 필요한지 판단하는 데 사용됩니다.
* 항상 CIDR 표기법을 사용하세요. `203.0.113.10`처럼 슬래시 없는 IP는 클라우드 제공업체에서 거부합니다. `203.0.113.10/32`를 사용하세요.
* IPv6 범위의 경우 `/128`(단일 주소) 또는 더 큰 CIDR(예: `2001:db8::1/128`)을 사용하세요. 모든 클라우드 제공업체가 IPv6 소스 범위를 지원하지는 않으므로 해당 제공업체의 LoadBalancer 문서를 확인하세요.
* 목록은 **OR** 조건입니다. 소스가 어느 항목과도 일치하면 트래픽이 허용됩니다.

파일 편집 후 아래의 `helm install`을 진행하세요. 컨트롤러가 이미 설치되어 있다면 같은 플래그로 `helm upgrade`를 실행하거나 런타임에 Service를 직접 패치하세요 (다음 섹션 참조).

#### 런타임에 허용 목록 업데이트

Helm 업그레이드 없이 Service를 직접 패치하여 허용 IP를 변경할 수 있습니다. **패치는 전체 목록을 교체합니다.** 새 IP만 포함하지 말고 유지하고 싶은 모든 IP를 반드시 포함하세요.

새 IP 집합으로 목록을 교체하려면:

```bash theme={null}
kubectl patch svc traefik-dashboard -n traefik-dashboard \
  -p '{"spec":{"loadBalancerSourceRanges":["203.0.113.10/32","198.51.100.0/24","192.0.2.50/32"]}}'
```

기존 항목을 잃지 않고 IP를 안전하게 **추가**하려면 먼저 현재 목록을 읽은 후 합쳐진 집합으로 패치하세요:

```bash theme={null}
# 1. 현재 허용 목록 확인
kubectl get svc traefik-dashboard -n traefik-dashboard \
  -o jsonpath='{.spec.loadBalancerSourceRanges}'

# 2. 새 IP를 포함한 전체 목록으로 패치
kubectl patch svc traefik-dashboard -n traefik-dashboard \
  -p '{"spec":{"loadBalancerSourceRanges":["<EXISTING_IP_1>/32","<EXISTING_IP_2>/32","<NEW_IP>/32"]}}'
```

> 런타임 패치는 `values-dashboard.yaml`에 저장되지 않습니다. 향후 Helm 업그레이드 이후에도 변경 사항을 유지하려면 값 파일도 업데이트하고 커밋하세요.

그런 다음 설치:

```bash theme={null}
helm install traefik-dashboard traefik/traefik \
  --namespace traefik-dashboard --create-namespace \
  --version 39.0.8 \
  -f third-party/traefik/values-dashboard.yaml
```

**확인:**

```bash theme={null}
kubectl get pods -n traefik-dashboard
```

예상 결과: 1개 파드가 `Running` 상태.

```bash theme={null}
kubectl get ingressclass traefik-dashboard
```

예상 결과: IngressClass가 존재해야 합니다.

***

### 1.4 LoadBalancer 대기

두 Traefik 인스턴스 모두 진행하기 전에 외부 IP가 필요합니다.

```bash theme={null}
kubectl get svc -n traefik-public
kubectl get svc -n traefik-dashboard
```

**확인:** 두 서비스 모두 `EXTERNAL-IP`가 표시되어야 합니다 (`<pending>` 상태가 아닌 것).

여전히 pending 상태라면 할당을 대기하세요:

```bash theme={null}
kubectl get svc -n traefik-public -w
```

IP가 표시되면 `Ctrl+C`를 누르세요. IP 할당은 일반적으로 2\~5분이 소요됩니다.

**실패 시:** 10분 후에도 `<pending>` 상태이면 일반적으로 클라우드 제공업체가 LoadBalancer를 프로비저닝할 수 없는 것입니다. 서브넷 태그(EKS는 `kubernetes.io/role/elb` 필요), VPC 구성, 서비스 할당량, 내부 LB 어노테이션이 올바르게 설정되어 있는지 확인하세요.

***

## Phase 2 -- 시크릿 생성 (\~10분)

모든 시크릿은 애플리케이션을 배포하기 전에 수동으로 생성합니다. 이렇게 하면 민감한 값이 매니페스트 파일에 노출되지 않습니다.

### 2.1 네임스페이스 생성

```bash theme={null}
kubectl create namespace agenteye
```

**확인:**

```bash theme={null}
kubectl get namespace agenteye
```

예상 결과: 상태가 `Active`.

***

### 2.2 이미지 풀 시크릿

이 시크릿은 AgentEye 컨테이너 이미지를 가져오기 위해 `ghcr.io`에 인증합니다. PAT 생성 방법은 [enterprise-docs/github-token.md](/ko/agenteye/github-token)를 참조하세요.

```bash theme={null}
kubectl create secret docker-registry agenteye-image-pull \
  --namespace agenteye \
  --docker-server=ghcr.io \
  --docker-username=agenteye-enterprise \
  --docker-password="${AGENTEYE_TOKEN}"
```

**확인:**

```bash theme={null}
kubectl get secret agenteye-image-pull -n agenteye -o jsonpath='{.type}'
```

예상 결과: `kubernetes.io/dockerconfigjson`.

**심층 확인** -- 토큰이 실제로 이미지를 풀할 수 있는지 확인:

overlay의 `kustomization.yaml`에 고정된 `server` 이미지 태그를 사용하세요 (현재 번들된 `acme` overlay와 base 배포 모두 `v0.0.1-beta.48`). 릴리스 간 이 확인이 어긋나지 않도록 아래 태그를 배포할 태그로 교체하세요:

```bash theme={null}
kubectl run test-pull \
  --image=ghcr.io/agenteye-enterprise/server:v0.0.1-beta.48 \
  --overrides='{"spec":{"imagePullSecrets":[{"name":"agenteye-image-pull"}]}}' \
  --restart=Never -n agenteye --command -- echo ok

# 풀이 완료될 때까지 몇 초 기다린 후:
kubectl logs test-pull -n agenteye
kubectl delete pod test-pull -n agenteye
```

예상 결과: 로그에 `ok`가 출력되어야 합니다.

**실패 시:** `ErrImagePull` 또는 `401 Unauthorized`는 PAT가 유효하지 않거나 `read:packages` 스코프가 없는 것입니다. [enterprise-docs/github-token.md](/ko/agenteye/github-token)를 다시 확인하세요.

***

### 2.3 PostgreSQL 자격 증명

```bash theme={null}
POSTGRES_PASSWORD=$(openssl rand -hex 24)

kubectl create secret generic agenteye-postgres \
  --namespace agenteye \
  --from-literal=POSTGRES_USER=postgres \
  --from-literal=POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
  --from-literal=POSTGRES_DB=agenteye
```

> **중요:** 비밀번호 생성 시 `-base64`가 아닌 `-hex`를 사용합니다. Base64 출력에는 `+`, `/`, `=`가 포함될 수 있으며, 이는 `DATABASE_URL` 연결 문자열을 깨뜨립니다. 자세한 내용은 [enterprise-docs/troubleshooting.md](/ko/agenteye/troubleshooting)를 참조하세요.

> **`POSTGRES_PASSWORD`를 즉시 시크릿 매니저에 저장하세요.** 백업에서 복원하거나 데이터베이스에 직접 연결할 때 필요합니다.

**확인:**

```bash theme={null}
kubectl get secret agenteye-postgres -n agenteye
```

예상 결과: 시크릿이 존재해야 합니다.

```bash theme={null}
kubectl get secret agenteye-postgres -n agenteye \
  -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
```

예상 결과: `48` (24 hex 바이트 = 48자).

***

### 2.4 관리자 API 키

```bash theme={null}
ADMIN_KEY=$(openssl rand -hex 32)

kubectl create secret generic agenteye-admin-key \
  --namespace agenteye \
  --from-literal=ADMIN_KEY="${ADMIN_KEY}"
```

관리자 키는 부트스트랩 자격 증명입니다. 서버는 시작 시마다 모든 권한으로 이 키를 upsert합니다. Phase 7에서 이 키를 사용하여 스코프가 지정된 수집기 키를 생성하세요. 전체 권한 모델은 [enterprise-docs/api-keys.md](/ko/agenteye/api-keys)를 참조하세요.

> **`ADMIN_KEY`를 즉시 시크릿 매니저에 저장하세요.**

**확인:**

```bash theme={null}
kubectl get secret agenteye-admin-key -n agenteye
```

예상 결과: 시크릿이 존재해야 합니다.

***

### 2.5 인증 구성 (대시보드 로그인)

대시보드는 사용자 로그인에 이메일 + OTP를 사용합니다. 이 시크릿이 없어도 서버는 시작되고 `ADMIN_KEY` API 경로는 계속 동작하지만, **사용자가 UI를 통해 로그인할 수 없습니다.**

모든 키는 base 매니페스트에서 `optional: true`로 참조되므로, 일부 시크릿만 있거나 시크릿이 전혀 없어도 됩니다. 서버는 문서화된 기본값으로 폴백합니다. 모든 것을 하나의 `agenteye-auth` 시크릿으로 묶으면 인증 관련 값을 한 곳에서 교체할 수 있습니다.

```bash theme={null}
kubectl create secret generic agenteye-auth \
  --namespace agenteye \
  --from-literal=ADMIN_EMAIL="admin@yourcompany.com" \
  --from-literal=ALLOWED_EMAILS="*@yourcompany.com" \
  --from-literal=SMTP_HOST="smtp.yourprovider.com" \
  --from-literal=SMTP_PORT="587" \
  --from-literal=SMTP_USERNAME="your-smtp-user" \
  --from-literal=SMTP_PASSWORD="your-smtp-password" \
  --from-literal=SMTP_FROM="noreply@yourcompany.com" \
  --from-literal=SMTP_TLS="starttls" \
  --from-literal=DEFAULT_ORG_NAME="Acme Corp" \
  --from-literal=DEFAULT_ORG_SLUG="acme"
```

| 키                                                                       | 목적                                                                                                                                                                                                                                                                      |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ADMIN_EMAIL`                                                           | 부트스트랩 관리자 사용자. 시작 시마다 모든 권한으로 upsert되며, 대시보드를 통한 삭제/권한 수정이 차단됩니다. 설정하지 않으면 관리자가 시드되지 않아 첫 번째 로그인이 불가능합니다.                                                                                                                                                               |
| `ALLOWED_EMAILS`                                                        | 쉼표로 구분된 허용 목록. 정확한 주소(`user@example.com`)와 도메인 와일드카드(`*@example.com`)를 지원합니다. 설정하지 않으면 **사용자 로그인 또는 생성이 불가능합니다.**                                                                                                                                                       |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_FROM` | OTP 코드 발송을 위한 SMTP 릴레이. `SMTP_HOST`가 설정되지 않으면 OTP 코드는 이메일 대신 서버 stdout에 기록됩니다 (최초 부팅 스모크 테스트에 유용). 실제 이메일 발송을 위해서는 모든 SMTP 키를 함께 제공하세요.                                                                                                                                 |
| `SMTP_TLS`                                                              | `starttls`(기본값), `tls`, 또는 `none` 중 하나.                                                                                                                                                                                                                                 |
| `DEFAULT_ORG_NAME`, `DEFAULT_ORG_SLUG`                                  | 선택 사항. 내장된 `default` 조직에 친숙한 표시 이름과 URL 슬러그를 지정하여 `/default` 대신 `/acme`와 같은 경로에서 사용할 수 있게 합니다. **최초 부팅 시에만 적용되며**, `agenteye-orgctl org rename`으로 조직 이름을 변경하면 이후에는 무시됩니다 (§7.6 참조). 슬러그는 1\~40자의 소문자 영숫자로, 중간에 단일 하이픈을 사용할 수 있습니다. 기본 `default`를 유지하려면 두 값 모두 설정하지 마세요. |

> **SMTP 자격 증명을 시크릿 매니저에 저장하세요.**

**확인:**

```bash theme={null}
kubectl get secret agenteye-auth -n agenteye \
  -o jsonpath='{.data}' | grep -o '"[A-Z_]*"' | sort -u
```

예상 결과: 입력한 키들이 출력에 나타나야 합니다.

***

### 2.6 멀티 테넌트 조직 격리 키 (선택 사항)

단일 테넌트 배포에서는 건너뛰세요. 서버는 내장된 개발용 기본값으로 실행되며, 단일 `default` 조직을 정상적으로 제공합니다. **두 번째 조직을 생성하기 전에** 강력하고 안정적인 `ORG_CH_SECRET`을 설정하세요. 각 조직의 ClickHouse 비밀번호는 `HMAC(ORG_CH_SECRET, org_id)`로 파생되므로, 공개적으로 알려진 개발용 기본값을 사용하면 조직별 자격 증명이 공개적으로 유추 가능해집니다. `agenteye-orgctl org create` 명령(§7.6 참조)은 서버가 내장된 개발용 기본값을 사용하는 동안에는 실행을 거부합니다.

```bash theme={null}
kubectl create secret generic agenteye-org-ch-secret \
  --namespace agenteye \
  --from-literal=ORG_CH_SECRET="$(openssl rand -base64 48)"

# 서버를 롤아웃 재시작하여 새 값을 적용합니다.
kubectl -n agenteye rollout restart deployment/server
```

서버는 이 값을 **선택적** `secretKeyRef`로 읽으므로, 이 시크릿을 생성하지 않은 단일 테넌트 클러스터도 정상적으로 부팅됩니다. 값을 **안정적으로 유지하고 모든 레플리카에서 동일하게** 유지하세요. 교체 시 부팅 시 재조정이 사용자를 다시 프로비저닝할 때까지 모든 조직의 파생된 ClickHouse 비밀번호가 무효화됩니다 (값이 모든 곳에서 일치하는 롤링 재시작으로 복구됩니다). `deploy/base/server/secret.example.yaml`을 참조하세요.

> **`ORG_CH_SECRET`을 시크릿 매니저에 저장하고 불필요하게 교체하지 마세요.**

***

### 2.7 모든 시크릿 확인

```bash theme={null}
kubectl get secrets -n agenteye -o custom-columns='NAME:.metadata.name,TYPE:.type'
```

예상 출력 (기본 시크릿 외에):

```
NAME                    TYPE
agenteye-admin-key      Opaque
agenteye-auth           Opaque
agenteye-image-pull     kubernetes.io/dockerconfigjson
agenteye-postgres       Opaque
agenteye-org-ch-secret  Opaque   # §2.6(멀티 테넌트)을 완료한 경우에만
```

4개의 핵심 시크릿(`agenteye-admin-key`, `agenteye-auth`, `agenteye-image-pull`, `agenteye-postgres`)은 계속하기 전에 반드시 존재해야 합니다. `agenteye-org-ch-secret`은 멀티 테넌트 배포에서만 필요합니다 (§2.6 참조).

***

## Phase 3 -- 애플리케이션 배포 (\~5분)

### 3.1 공용 호스트명 구성

cert-manager가 Let's Encrypt 인증서를 요청하려면 수집 및 대시보드 호스트명이 필요합니다. 템플릿을 복사하고 두 값을 설정하세요:

```bash theme={null}
cp base/certificates/domain.env.example base/certificates/domain.env
# base/certificates/domain.env를 편집하고 다음을 설정하세요:
#   INGEST_DOMAIN=ingest.your-company.example      (공용 Traefik LB로 해석됨)
#   DASHBOARD_DOMAIN=agenteye.your-company.example (대시보드 Traefik LB로 해석됨)
```

`domain.env`는 gitignore에 포함되어 있으므로 각 배포에 로컬로 유지됩니다. 둘 중 하나라도 누락되면 kustomize 빌드가 오류를 발생시킵니다.

> **DNS가 먼저 해석되어야 합니다.** 아직 DNS를 LB에 연결하지 않아도 됩니다 (Phase 1.2가 완료될 때까지 LB가 존재하지 않음). 하지만 단계 3.2의 ACME 발급은 각 호스트명이 해당 LoadBalancer로 해석될 때까지 재시도합니다. Phase 1.4에서 캡처한 LB 호스트명을 사용하여 지금 DNS를 설정하거나, Phase 4에서 레코드를 추가할 수 있습니다.

***

### 3.2 매니페스트 적용

신규 설치 시 base를 직접 적용하거나, 해당 환경용 overlay가 있다면 overlay를 적용하세요 (overlay는 이미지 태그, 환경 변수, 리소스 제한만 고정하며 base의 인증서와 라우팅을 상속합니다):

```bash theme={null}
kubectl apply -k base/
# 또는
kubectl apply -k overlays/<your-env>/
```

overlay는 base를 자동으로 포함하므로 둘 다 적용하지 마세요.

***

### 3.3 파드 대기

```bash theme={null}
kubectl wait --for=condition=Ready pod -l 'app in (server,dashboard,postgres,clickhouse)' \
  -n agenteye --timeout=180s
```

대기는 핵심 데이터 플레인 파드로 범위가 지정됩니다. 선택적 `agent`(AI 어시스턴트)와 `redis` 파드는 함께 시작됩니다. 어시스턴트는 LLM 엔드포인트를 제공할 때까지 비활성 상태이고 ([enterprise-docs/assistant.md](/ko/agenteye/assistant) 참조), Redis는 최선 노력 캐시이므로, 플랫폼이 트래픽을 처리하기 위해 둘 다 Ready 상태일 필요는 없습니다.

**확인:**

```bash theme={null}
kubectl get pods -n agenteye
```

예상 결과 (선택적 `agent` 및 `redis` 파드도 함께 표시되어 `Running` 상태가 됨):

```
NAME                         READY   STATUS    RESTARTS   AGE
agent-xxxxxxxxxx-xxxxx       1/1     Running   0          ...
clickhouse-0                 1/1     Running   0          ...
dashboard-xxxxxxxxxx-xxxxx   1/1     Running   0          ...
dashboard-xxxxxxxxxx-xxxxx   1/1     Running   0          ...
postgres-0                   1/1     Running   0          ...
redis-0                      1/1     Running   0          ...
server-xxxxxxxxxx-xxxxx      1/1     Running   0          ...
server-xxxxxxxxxx-xxxxx      1/1     Running   0          ...
```

**실패 시:**

| 파드 상태              | 가능한 원인                       | 디버그 명령                                                |
| ------------------ | ---------------------------- | ----------------------------------------------------- |
| `ImagePullBackOff` | 이미지 풀 시크릿 또는 PAT 오류          | `kubectl describe pod <name> -n agenteye`             |
| `CrashLoopBackOff` | 잘못된 환경 변수 (예: DATABASE\_URL) | `kubectl logs <name> -n agenteye`                     |
| `Pending`          | CPU/메모리 부족 또는 노드 없음          | `kubectl describe pod <name> -n agenteye` (Events 확인) |

***

### 3.4 스토리지 확인

```bash theme={null}
kubectl get pvc -n agenteye
```

예상 결과, 두 항목 모두 `Bound` 상태:

| PVC                            | 용량      | 대상                         |
| ------------------------------ | ------- | -------------------------- |
| `postgres-data-postgres-0`     | `50Gi`  | PostgreSQL 관계형/메타데이터 저장소   |
| `clickhouse-data-clickhouse-0` | `100Gi` | ClickHouse 이벤트 + 평가 분석 저장소 |

선택적 캐시용 `redis-data-redis-0` PVC(1Gi)도 함께 표시됩니다.

**실패 시:** `Pending`은 StorageClass가 볼륨을 프로비저닝할 수 없다는 의미입니다. `kubectl get storageclass`를 확인하고 기본값이 있는지 확인하세요. 프로덕션에서는 overlay로 ClickHouse 볼륨을 빠른 SSD StorageClass(예: AWS의 gp3, GCP의 pd-ssd)에 마운트하세요. 느린 디스크에서는 컴팩션 처리량이 저하됩니다.

***

### 3.5 인증서 확인

```bash theme={null}
kubectl get certificates -n agenteye
```

예상 결과: 3개 인증서, 모두 `Ready: True`:

| 이름              | 발급자                | 목적                                     |
| --------------- | ------------------ | -------------------------------------- |
| `mtls-ca`       | `selfsigned`       | mTLS 클라이언트 인증서 발급을 위한 프라이빗 CA (10년 유효) |
| `ingest-tls`    | `letsencrypt-prod` | 수집 엔드포인트용 공용 TLS 인증서 (90일, 자동 갱신)      |
| `dashboard-tls` | `letsencrypt-prod` | 대시보드용 공용 TLS 인증서 (90일, 자동 갱신)          |

**`ingest-tls` 또는 `dashboard-tls`가 Ready 상태가 아닌 경우:**

`kubectl describe certificate <name> -n agenteye`를 실행하고 Events를 확인하세요. 일반적인 원인:

* **DNS가 아직 LB를 가리키지 않음.** Let's Encrypt는 호스트명을 해석하고 포트 80에 연결하여 검증합니다 — `INGEST_DOMAIN`은 공용 LB로, `DASHBOARD_DOMAIN`은 대시보드 LB로 해석되어야 합니다. CNAME/Alias가 전파될 때까지 주문은 `pending` 상태로 유지됩니다. DNS가 올바르게 설정되면 cert-manager가 자동으로 재시도합니다 (Certificate를 삭제할 필요 없음).
* **호스트명이 치환되지 않음.** `dnsNames`가 여전히 `INGEST_DOMAIN_PLACEHOLDER` / `DASHBOARD_DOMAIN_PLACEHOLDER`로 표시되면 3.1 단계를 건너뛴 것입니다 — `base/certificates/domain.env`를 생성하고 다시 적용하세요.
* **대시보드 Traefik이 챌린지를 처리할 수 없음** (`dashboard-tls`만 해당). 대시보드 Traefik 인스턴스는 번들된 값 파일(Phase 1.2)과 함께 설치되어야 합니다. 이 파일은 cert-manager의 HTTP-01 솔버를 처리하는 스코프가 지정된 Ingress 제공자를 활성화합니다. 이 파일 없이 설치된 인스턴스는 챌린지를 라우팅할 수 없어 주문이 영원히 `pending` 상태로 남습니다.

**`mtls-ca`가 Ready 상태가 아닌 경우:** cert-manager 자체가 비정상 상태입니다. 1.1 단계의 cert-manager 파드를 다시 확인하세요.

***

### 3.6 CronJob 확인

```bash theme={null}
kubectl get cronjobs -n agenteye
```

예상 결과:

| 이름                   | 스케줄            | 목적                                     |
| -------------------- | -------------- | -------------------------------------- |
| `agenteye-backup`    | `0 3 * * *`    | UTC 03:00에 매일 Postgres + ClickHouse 백업 |
| `cert-renewal-check` | `0 3,15 * * *` | UTC 03:00 및 15:00에 인증서 만료 알림           |

***

### 3.7 서버 정상 시작 확인

```bash theme={null}
kubectl logs -n agenteye -l app=server --tail=20
```

**확인:** 서버가 포트 8080에서 수신 대기 중임을 나타내는 시작 로그를 찾으세요. 데이터베이스 연결 오류가 없어야 합니다 (서버는 Ready를 보고하기 전에 PostgreSQL과 ClickHouse 모두 도달 가능한지 확인합니다).

**실패 시:** 가장 일반적인 원인은 URL에 안전하지 않은 문자가 포함된 `POSTGRES_PASSWORD`가 `DATABASE_URL`을 깨뜨리는 경우입니다. [enterprise-docs/troubleshooting.md](/ko/agenteye/troubleshooting)를 참조하세요.

***

### 3.8 대시보드가 서버에 연결되었는지 확인

```bash theme={null}
kubectl logs -n agenteye -l app=dashboard --tail=20
```

**확인:** 출력에서 `Ready`를 찾고 `ECONNREFUSED` 또는 유사한 오류가 없는지 확인하세요.

**실패 시:** `server` Service가 존재하는지 확인하고 (`kubectl get svc server -n agenteye`), 대시보드 배포에서 `AGENTEYE_SERVER_URL`이 `http://server:8080`으로 설정되어 있는지 확인하세요.

***

## Phase 4 -- 네트워크 접근 (\~5분)

### 4.1 LoadBalancer 주소 확인

```bash theme={null}
PUBLIC_IP=$(kubectl get svc -n traefik-public \
  -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')

INTERNAL_IP=$(kubectl get svc -n traefik-dashboard \
  -o jsonpath='{.items[0].status.loadBalancer.ingress[0].ip}')
```

> AWS EKS에서는 LoadBalancer가 IP 대신 호스트명을 반환합니다. 위 명령에서 `.ip`를 `.hostname`으로 교체하세요.

**확인:**

```bash theme={null}
echo "Public  (ingest):    $PUBLIC_IP"
echo "Internal (dashboard): $INTERNAL_IP"
```

두 값 모두 비어 있지 않아야 합니다.

***

### 4.2 LoadBalancer에 DNS 연결

`base/certificates/domain.env`의 호스트명이 각 LoadBalancer로 해석되도록 DNS 레코드를 생성하세요 — `INGEST_DOMAIN`은 **공용** Traefik LB로, `DASHBOARD_DOMAIN`은 **대시보드** Traefik LB로:

* **AWS Route 53:** `Alias = Yes`로 `A` 레코드 생성, 대상 = LB 호스트명. 일반 A → IP는 사용하지 마세요. ELB IP는 교체됩니다.
* **다른 제공업체:** 호스트명에서 LB 호스트명으로 `CNAME` 생성.

확인:

```bash theme={null}
dig +short ingest.your-company.example
dig +short agenteye.your-company.example
```

각각 `$PUBLIC_IP` 및 `$INTERNAL_IP`와 동일한 주소가 반환되어야 합니다 (EKS에서는 동일한 `*.elb.amazonaws.com` 호스트명으로 해석).

DNS가 해석되면 cert-manager는 Phase 3.5의 보류 중인 ACME 주문을 1분 이내에 완료합니다. `ingest-tls`와 `dashboard-tls` 모두 `Ready: True`가 될 때까지 `kubectl get certificates -n agenteye`를 다시 실행하세요.

***

### 4.3 수집 엔드포인트 접근

공용 수집 엔드포인트는 상호 TLS를 강제하므로 `/health`를 포함한 모든 요청에 클라이언트 인증서가 필요합니다. 첫 번째 클라이언트 인증서는 Phase 5에서 발급합니다. 이미 인증서가 있다면 지금 접근 가능 여부를 확인하세요:

```bash theme={null}
curl -s --cert issued/<cluster-name>/client.crt \
        --key issued/<cluster-name>/client.key \
        https://ingest.your-company.example/health
```

예상 결과: `{"status":"ok"}`. `-k`는 필요하지 않습니다 — 서버 인증서는 `INGEST_DOMAIN`에 대해 공용 CA에 체인되어 있으므로 시스템 신뢰 저장소에서 검증됩니다. 원시 LoadBalancer IP/호스트명이 아닌 `INGEST_DOMAIN` 호스트명으로 수집 엔드포인트에 접근하세요 (발급된 인증서와 호스트명이 일치해야 함).

대시보드 엔드포인트는 `DASHBOARD_DOMAIN`에서 공개 신뢰 인증서로 제공되며 mTLS 뒤에 있지 않으므로, `-k`와 클라이언트 인증서가 필요하지 않습니다:

```bash theme={null}
curl -s https://agenteye.your-company.example/ -o /dev/null -w '%{http_code}\n'
```

원시 LB 주소가 아닌 호스트명으로 대시보드에 접근하세요 — 인증서는 `DASHBOARD_DOMAIN`에 바인딩되어 있으므로 원시 주소는 인증서 이름 불일치를 표시합니다.

**실패 시:** `curl`이 멈추면 해당 머신에서 LB에 접근 가능한지 확인하세요 (VPN, 보안 그룹, 방화벽 규칙). 수집 호스트명에서 `certificate required` 핸드셰이크 오류는 클라이언트 인증서가 제공되지 않은 것입니다. Phase 5를 먼저 완료하세요. 수집 호스트명에서 TLS 유효성 검사 오류는 서버 인증서 발급이 완료되지 않은 것입니다. Phase 3.5로 돌아가서 해결하세요.

***

## Phase 5 -- mTLS 클라이언트 인증서 발급 (\~클러스터당 10분)

수집기는 **두 가지 요소**로 인증합니다: 클라이언트 인증서(전송 계층, 요청이 승인된 클러스터에서 온 것을 증명)와 API 키(애플리케이션 계층, 요청이 `events:add` 권한을 가진 수집기에서 온 것을 증명). 유출된 키는 인증서 없이 쓸모없고, 도난당한 인증서는 유효한 키 없이 쓸모없습니다.

### 5.1 인증서 발급

수집기를 실행하는 각 클러스터에는 고유한 클라이언트 인증서가 필요합니다. 매니페스트 디렉터리에서:

```bash theme={null}
cd base/certificates/client-certs
./issue-client-cert.sh <cluster-name>
```

`<cluster-name>`을 의미 있는 식별자(예: `us-east-1-prod`, `staging`)로 교체하세요.

**확인:** 스크립트가 `==> Done!`을 출력하고 출력 파일 목록을 나열합니다.

```bash theme={null}
kubectl get certificate mtls-client-<cluster-name> -n agenteye
```

예상 결과: `Ready: True`.

`issued/<cluster-name>/`의 출력 파일:

| 파일                           | 목적                                    |
| ---------------------------- | ------------------------------------- |
| `client.crt`                 | 클라이언트 인증서 (90일 유효)                    |
| `client.key`                 | 클라이언트 개인 키                            |
| `ca.crt`                     | 서버 검증을 위한 CA 인증서                      |
| `collector-mtls-secret.yaml` | 수집기 클러스터에 바로 적용 가능한 Kubernetes Secret |

***

### 5.1b 대체 전달 방법: AWS Secrets Manager

인증서 소비자가 `client.crt`와 `client.key`를 디스크에 필요로 하는 Kubernetes Pod인 경우 — agenteye-collector를 애플리케이션 파드의 사이드카로 실행할 때의 일반적인 경우 — 인증서 번들을 AWS Secrets Manager에 저장하세요. 그러면 애플리케이션 파드는 IRSA와 함께 [Secrets Store CSI Driver](https://secrets-store-csi-driver.sigs.k8s.io/)를 통해 마운트하고, 인증서 교체가 자동으로 처리됩니다.

```bash theme={null}
cd base/certificates/client-certs
export AWS_REGION=us-east-1     # 워크로드가 실행되는 리전
./issue-client-cert.sh <cluster-name> --save-to aws-secrets-manager
```

재실행(갱신) 시 스크립트는 동일한 시크릿에 `PutSecretValue`를 호출하므로 ARN과 이름이 안정적으로 유지됩니다. CSI Driver는 다음 교체 폴링 시 새 버전을 가져와 파드 내부 파일을 덮어씁니다.

**사전 요구 사항:**

* AWS 계정에 인증된 `aws` CLI v2.
* `jq` 설치됨.
* `AWS_REGION` 환경 변수 설정됨.
* 호출자 ID의 IAM 권한 (`Resource`를 `arn:aws:secretsmanager:<region>:<account>:secret:agenteye/mtls-client/*`로 제한):
  * `secretsmanager:CreateSecret`
  * `secretsmanager:DescribeSecret`
  * `secretsmanager:PutSecretValue`
  * `secretsmanager:TagResource`

**이 모드에서 스크립트가 하는 작업:**

| 단계 | 작업                                                                                                                                                                            |
| -- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1  | cert-manager를 통해 인증서를 발급/재추출합니다 (기본 모드와 동일).                                                                                                                                  |
| 2  | `agenteye/mtls-client/<cluster-name>`에서 `DescribeSecret`을 호출하여 생성 또는 업데이트를 결정합니다.                                                                                             |
| 3  | 최초 실행: 세 개의 키 JSON 페이로드(`client.crt`, `client.key`, `ca.crt`)와 `AgentEyeCluster=<cluster-name>` 태그로 `CreateSecret`. 이후 실행: 새 버전을 게시하는 `PutSecretValue`; `TagResource`로 태그 갱신. |
| 4  | 성공적인 업로드 후에만 `issued/<cluster-name>/`를 삭제합니다. 실패 시 디렉터리를 보존하여 재시도할 수 있습니다.                                                                                                    |

**시크릿이 삭제 예정인 경우**, 스크립트는 재시도 전에 `aws secretsmanager restore-secret --secret-id agenteye/mtls-client/<cluster-name>`을 실행하라는 명확한 오류 메시지와 함께 실패합니다.

전체 파드 연결 설정(SecretProviderClass, IRSA 설정, 교체 동작, 문제 해결)은 [enterprise-docs/single-pod-deployment.md](/ko/agenteye/single-pod-deployment)를 참조하세요.

***

### 5.2 인증서 동작 확인

mTLS 인그레스에 발급된 인증서를 테스트하세요:

```bash theme={null}
curl -sk --cert issued/<cluster-name>/client.crt \
     --key issued/<cluster-name>/client.key \
     https://${PUBLIC_IP}/health
```

예상 결과: `{"status":"ok"}`

**실패 시:**

| 오류                     | 원인                    | 해결 방법                                                                                          |
| ---------------------- | --------------------- | ---------------------------------------------------------------------------------------------- |
| `certificate required` | 인증서가 제공되지 않음          | `curl` 명령의 파일 경로 확인                                                                            |
| `bad certificate`      | CA 불일치                | `mtls-ca-issuer`가 인증서를 발급했는지 확인: `kubectl describe certificate mtls-client-<name> -n agenteye` |
| `connection refused`   | 잘못된 호스트명 또는 LB에 접근 불가 | `/etc/hosts` 또는 DNS 확인                                                                         |

***

### 5.3 수집기 클러스터에 전달

`collector-mtls-secret.yaml`을 수집기 클러스터를 운영하는 팀에 전달하세요. 팀은 이를 적용합니다:

```bash theme={null}
kubectl apply -f collector-mtls-secret.yaml -n <collector-namespace>
```

그런 다음 수집기가 시크릿을 마운트하고 인증서 경로를 사용하도록 구성하세요:

```json theme={null}
{
  "tls_cert": "/etc/agenteye/tls/client.crt",
  "tls_key": "/etc/agenteye/tls/client.key"
}
```

Kubernetes 볼륨 마운트를 포함한 완전한 수집기 설정은 [enterprise-docs/collector-installation.md](/ko/agenteye/collector-installation)를 참조하세요.

**확인 (수집기 클러스터에서):**

```bash theme={null}
kubectl get secret agenteye-collector-mtls -n <collector-namespace>
```

예상 결과: 3개의 데이터 키(`client.crt`, `client.key`, `ca.crt`)를 가진 시크릿이 존재해야 합니다.

***

### 5.4 인증서 수명 주기

| 속성              | 값                               |
| --------------- | ------------------------------- |
| 클라이언트 인증서 유효 기간 | 90일                             |
| 자동 갱신           | cert-manager가 만료 15일 전에 갱신      |
| CA 유효 기간        | 10년                             |
| 만료 알림           | CronJob이 만료 30일 전에 알림 (Phase 6) |

cert-manager는 **AgentEye 클러스터**에서 인증서를 자동으로 갱신하지만, 갱신된 인증서는 수집기 클러스터에 다시 전달해야 합니다. 이전 인증
