Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/prompts/0단계 완성기준 명세 요청.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
0단계 프롬프트 — 완성 기준 명세 (코드·설계 전)

[기능명]의 "완성 기준"을 먼저 확정한다. 코드도 설계도 아직 손대지 마라.
[기능명]의 "완성 기준"을 먼저 확정한다. 코드도 설계도 아직 작성하지 마라.

## 목적
무엇이 "되면 끝"인지 기능 단위로 먼저 못박는다. 특히 이 기능이 데이터를
Expand Down
77 changes: 58 additions & 19 deletions docs/runbooks/gcp-performance-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,25 @@ cp infra/terraform/envs/gcp-perf/terraform.tfvars.example \
- `region`
- `zone`
- `name_prefix`
- `image_tag`
- `image_tags`
- `db_password`
- `jwt_secret_key`
- subnet CIDR
- 임베딩 VM machine type과 disk 크기
- `model_artifact_path`

현재 모든 서비스가 하나의 `image_tag`를 공유한다. 빌드한 image tag와 `terraform.tfvars`의 `image_tag`가 반드시 같아야 한다.
각 image는 독립적인 tag를 사용한다. 처음 배포할 때는 모두 같은 tag로 시작해도 된다.

```hcl
image_tag = "<GIT_SHA_OR_RELEASE_TAG>"
image_tags = {
core_api = "<GIT_SHA_OR_RELEASE_TAG>"
search_service = "<GIT_SHA_OR_RELEASE_TAG>"
frontend = "<GIT_SHA_OR_RELEASE_TAG>"
managed_embedding_endpoint = "<GIT_SHA_OR_RELEASE_TAG>"
pipeline_worker = "<GIT_SHA_OR_RELEASE_TAG>"
feedback_ingestion_pipeline = "<GIT_SHA_OR_RELEASE_TAG>"
feedback_loop_pipeline = "<GIT_SHA_OR_RELEASE_TAG>"
}
```

`terraform.tfvars`는 secret을 포함하므로 Git에 커밋하지 않는다.
Expand Down Expand Up @@ -181,10 +189,11 @@ terraform -chdir=infra/terraform/envs/gcp-perf apply \

### 3.3 전체 image 빌드와 push

현재 스크립트는 다음 image 6개를 같은 tag로 빌드한다.
인자를 생략하면 다음 image 7개를 같은 tag로 모두 빌드한다.

- `core-api`
- `search-service`
- `frontend`
- `managed-embedding-endpoint`
- `pipeline-worker`
- `feedback-ingestion-pipeline`
Expand All @@ -199,7 +208,7 @@ IMAGE_TAG="$IMAGE_TAG" \
bash scripts/deploy/build_and_push_images.sh
```

빌드한 `IMAGE_TAG`를 `terraform.tfvars`의 `image_tag`에도 입력한다.
빌드한 `IMAGE_TAG`를 `terraform.tfvars`의 `image_tags` 항목에도 입력한다.

### 3.4 DB와 migration Job 생성

Expand Down Expand Up @@ -538,31 +547,49 @@ gcloud storage rm "$latest" --project="$GCP_PROJECT_ID"

## 5. 코드 변경 후 재배포

### 5.1 중요한 제한
### 5.1 배포 원칙

현재 Terraform은 모든 서비스 image에 하나의 `image_tag`를 사용한다.
각 image tag는 `terraform.tfvars`의 `image_tags`에서 따로 관리한다.

일부 서비스만 새 image로 수동 배포한 상태에서 예전 `image_tag`로 전체 `terraform apply`를 실행하면, 수동 배포한 서비스가 예전 image로 돌아갈 수 있다.
코드가 변경된 image만 빌드하고 해당 tag만 바꾼다. 그 뒤에도 `-target`이 아닌 전체 plan과 apply를 실행한다. Terraform은 image 주소가 바뀐 서비스만 갱신한다.

재배포 전에 반드시 다음을 확인한다.

```bash
git rev-parse --short HEAD
grep '^image_tag' infra/terraform/envs/gcp-perf/terraform.tfvars
terraform -chdir=infra/terraform/envs/gcp-perf console \
<<< 'var.image_tags'
```

### 5.2 전체 서비스 재배포
### 5.2 일부 image 재배포

빌드할 image 이름을 스크립트 인자로 넘긴다. 여러 개를 한 번에 지정할 수도 있다.

```bash
export IMAGE_TAG=$(git rev-parse --short HEAD)

PROJECT_ID="$GCP_PROJECT_ID" \
REGION="$GCP_REGION" \
IMAGE_TAG="$IMAGE_TAG" \
bash scripts/deploy/build_and_push_images.sh
bash scripts/deploy/build_and_push_images.sh frontend
```

`terraform.tfvars`의 `image_tag`를 같은 값으로 변경한 뒤 plan과 apply를 실행한다.
두 image를 함께 빌드하는 예시는 다음과 같다.

```bash
PROJECT_ID="$GCP_PROJECT_ID" \
REGION="$GCP_REGION" \
IMAGE_TAG="$IMAGE_TAG" \
bash scripts/deploy/build_and_push_images.sh frontend core-api
```

빌드한 image에 해당하는 `image_tags` 값만 `IMAGE_TAG`와 같은 값으로 바꾼다. 예를 들어 frontend만 빌드했다면 기존 `image_tags` 블록에서 다음 한 줄의 값만 바꾼다. 나머지 항목은 기존 값을 유지한다.

```hcl
frontend = "<NEW_IMAGE_TAG>"
```

전체 plan에서 의도하지 않은 서비스 변경이나 VM 교체가 없는지 확인한 뒤 apply한다.

```bash
terraform -chdir=infra/terraform/envs/gcp-perf plan \
Expand All @@ -572,18 +599,30 @@ terraform -chdir=infra/terraform/envs/gcp-perf apply \
/tmp/biblio-gcp-perf.tfplan
```

DB schema가 변경됐다면 migration Job을 다시 실행한다.
frontend만 변경했다면 plan에 embedding VM의 `must be replaced`가 나타나면 안 된다.

### 5.3 전체 image 재배포

### 5.3 일부 서비스만 임시 검증
```bash
export IMAGE_TAG=$(git rev-parse --short HEAD)

일부 image만 별도 tag로 배포하는 방식은 임시 검증에만 사용한다.
PROJECT_ID="$GCP_PROJECT_ID" \
REGION="$GCP_REGION" \
IMAGE_TAG="$IMAGE_TAG" \
bash scripts/deploy/build_and_push_images.sh
```

검증이 끝나면 다음 둘 중 하나를 선택한다.
`terraform.tfvars`의 모든 `image_tags`를 같은 값으로 변경한 뒤 plan과 apply를 실행한다.

1. 전체 image를 같은 정식 tag로 다시 빌드하고 Terraform에 반영한다.
2. Terraform을 서비스별 image tag 구조로 개선한 뒤 정식 반영한다.
```bash
terraform -chdir=infra/terraform/envs/gcp-perf plan \
-out=/tmp/biblio-gcp-perf.tfplan

임시 tag 상태를 그대로 둔 채 전체 `terraform apply`를 실행하지 않는다.
terraform -chdir=infra/terraform/envs/gcp-perf apply \
/tmp/biblio-gcp-perf.tfplan
```

DB schema가 변경됐다면 migration Job을 다시 실행한다.

## 6. worker와 FIP 풀 가동

Expand Down
23 changes: 12 additions & 11 deletions infra/terraform/envs/gcp-perf/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ locals {
image_registry = "${var.region}-docker.pkg.dev/${var.project_id}/${local.repository_id}"

service_images = {
"core-api" = "${local.image_registry}/core-api:${var.image_tag}"
"search-service" = "${local.image_registry}/search-service:${var.image_tag}"
"frontend" = "${local.image_registry}/frontend:${var.image_tag}"
"managed-embedding-endpoint" = "${local.image_registry}/managed-embedding-endpoint:${var.image_tag}"
"pipeline-worker" = "${local.image_registry}/pipeline-worker:${var.image_tag}"
"feedback-ingestion-pipeline" = "${local.image_registry}/feedback-ingestion-pipeline:${var.image_tag}"
"feedback-loop-pipeline" = "${local.image_registry}/feedback-loop-pipeline:${var.image_tag}"
"core-api" = "${local.image_registry}/core-api:${var.image_tags.core_api}"
"search-service" = "${local.image_registry}/search-service:${var.image_tags.search_service}"
"frontend" = "${local.image_registry}/frontend:${var.image_tags.frontend}"
"managed-embedding-endpoint" = "${local.image_registry}/managed-embedding-endpoint:${var.image_tags.managed_embedding_endpoint}"
"pipeline-worker" = "${local.image_registry}/pipeline-worker:${var.image_tags.pipeline_worker}"
"feedback-ingestion-pipeline" = "${local.image_registry}/feedback-ingestion-pipeline:${var.image_tags.feedback_ingestion_pipeline}"
"feedback-loop-pipeline" = "${local.image_registry}/feedback-loop-pipeline:${var.image_tags.feedback_loop_pipeline}"
}

bucket_names = {
Expand Down Expand Up @@ -441,10 +441,11 @@ module "pipeline_worker" {
VISION_MAX_OUTPUT_TOKENS = "2048"
WORKER_CONCURRENCY = "4"
# 임베딩 VM의 wireproxy(WARP) SOCKS5. YouTube 트래픽만 이 프록시로 우회한다.
YOUTUBE_PROXY_URL = "socks5://${module.embedding_vm.private_ip}:1080"
GCS_VIDEO_BUCKET_NAME = module.object_storage.bucket_names.video
EMBEDDING_API_URL = local.embedding_vm_url
EMBEDDING_TIMEOUT_SEC = "60"
YOUTUBE_PROXY_URL = "socks5://${module.embedding_vm.private_ip}:1080"
GCS_VIDEO_BUCKET_NAME = module.object_storage.bucket_names.video
EMBEDDING_API_URL = local.embedding_vm_url
EMBEDDING_TIMEOUT_SEC = tostring(var.pipeline_embedding_timeout_sec)
EMBEDDING_BATCH_SIZE = tostring(var.pipeline_embedding_batch_size)
}

secret_env_vars = {
Expand Down
13 changes: 12 additions & 1 deletion infra/terraform/envs/gcp-perf/terraform.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ project_id = "biblio-perf-example"
region = "asia-northeast3"
zone = "asia-northeast3-a"
name_prefix = "biblio-perf"
image_tag = "0000000"

image_tags = {
core_api = "0000000"
search_service = "0000000"
frontend = "0000000"
managed_embedding_endpoint = "0000000"
pipeline_worker = "0000000"
feedback_ingestion_pipeline = "0000000"
feedback_loop_pipeline = "0000000"
}

db_password = "replace-with-postgres-password"
jwt_secret_key = "replace-with-jwt-secret-key"
Expand All @@ -14,6 +23,8 @@ embedding_subnet_cidr = "10.20.3.0/24"
enable_managed_embedding_cloud_run = false
embedding_vm_machine_type = "e2-standard-4"
embedding_vm_model_disk_size_gb = 100
pipeline_embedding_timeout_sec = 180
pipeline_embedding_batch_size = 16

# 모델 버전은 타임스탬프를 붙여 관리한다.
# 경로의 마지막 조각이 ModelRelease 버전 이름이 되므로, 버전마다 다른 이름을 쓴다.
Expand Down
42 changes: 40 additions & 2 deletions infra/terraform/envs/gcp-perf/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ variable "name_prefix" {
type = string
}

variable "image_tag" {
type = string
variable "image_tags" {
type = object({
core_api = string
search_service = string
frontend = string
managed_embedding_endpoint = string
pipeline_worker = string
feedback_ingestion_pipeline = string
feedback_loop_pipeline = string
})

description = "Image tag for each independently built application image."
}

variable "frontend_origin" {
Expand Down Expand Up @@ -105,6 +115,34 @@ variable "worker_max_instance_count" {
default = 1
}

variable "pipeline_embedding_timeout_sec" {
type = number
default = 180
description = "Timeout in seconds for each pipeline-worker embedding HTTP request."

validation {
condition = (
var.pipeline_embedding_timeout_sec > 0 &&
floor(var.pipeline_embedding_timeout_sec) == var.pipeline_embedding_timeout_sec
)
error_message = "pipeline_embedding_timeout_sec must be a positive integer."
}
}

variable "pipeline_embedding_batch_size" {
type = number
default = 16
description = "Maximum enriched texts sent in one pipeline-worker embedding request."

validation {
condition = (
var.pipeline_embedding_batch_size > 0 &&
floor(var.pipeline_embedding_batch_size) == var.pipeline_embedding_batch_size
)
error_message = "pipeline_embedding_batch_size must be a positive integer."
}
}

variable "cloudrun_subnet_cidr" {
type = string
default = "10.20.1.0/24"
Expand Down
44 changes: 44 additions & 0 deletions scripts/deploy/build_and_push_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,54 @@ services=(
"feedback-loop-pipeline:services/feedback-loop-pipeline:services/feedback-loop-pipeline/Dockerfile"
)

requested_services=("$@")

service_exists() {
local requested_name="$1"
local item

for item in "${services[@]}"; do
if [[ "${item%%:*}" == "${requested_name}" ]]; then
return 0
fi
done

return 1
}

should_build() {
local service_name="$1"
local requested_name

if (( ${#requested_services[@]} == 0 )); then
return 0
fi

for requested_name in "${requested_services[@]}"; do
if [[ "${requested_name}" == "${service_name}" ]]; then
return 0
fi
done

return 1
}

for requested_name in "${requested_services[@]}"; do
if ! service_exists "${requested_name}"; then
echo "Unknown service: ${requested_name}" >&2
echo "Available services: ${services[*]%%:*}" >&2
exit 2
fi
done

gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet

for item in "${services[@]}"; do
name="${item%%:*}"
if ! should_build "${name}"; then
continue
fi

rest="${item#*:}"
context="${rest%%:*}"
dockerfile="${rest#*:}"
Expand Down
18 changes: 18 additions & 0 deletions services/pipeline-worker/src/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
from src.config.settings import Settings
from src.schemas.messages import MessageType
from src.services.chunking_service import ChunkingService
from src.services.long_audio_transcription import LongAudioTranscriptionService
from src.services.pipeline_orchestrator import PipelineOrchestrator
from src.services.transcript_merge_service import TranscriptMergeService
from src.usecases.delete_project import DeleteProjectUseCase
from src.usecases.delete_video import DeleteVideoUseCase
from src.usecases.process_video import ProcessVideoUseCase
Expand Down Expand Up @@ -182,6 +184,18 @@ async def create_production_bootstrap(settings: Settings) -> None:
max_tokens=settings.chunk_max_tokens,
overlap_sentences=settings.chunk_overlap_sentences,
)
long_audio_transcription_service = LongAudioTranscriptionService(
artifact_repository=artifact_repo,
video_repository=video_repo,
storage_client=storage_client,
ffmpeg_client=ffmpeg_client,
stt_adapter=stt_adapter,
merge_service=TranscriptMergeService(),
part_duration_sec=settings.audio_part_duration_sec,
part_overlap_sec=settings.audio_part_overlap_sec,
stt_concurrency=settings.stt_part_concurrency,
processing_timeout_sec=settings.audio_processing_timeout_sec,
)

orchestrator = PipelineOrchestrator(
video_repository=video_repo,
Expand All @@ -194,10 +208,14 @@ async def create_production_bootstrap(settings: Settings) -> None:
vision_adapter=vision_adapter,
workdir_manager=workdir_manager,
chunking_service=chunking_service,
long_audio_transcription_service=long_audio_transcription_service,
embedding_batch_size=settings.embedding_batch_size,
stt_model_version=settings.stt_model_version or "chirp_2",
embedding_model_version=settings.embedding_model_version,
release_context_repository=release_context_repo,
max_audio_duration_sec=settings.max_audio_duration_sec,
max_source_size_bytes=settings.youtube_max_filesize_bytes,
audio_processing_timeout_sec=settings.audio_processing_timeout_sec,
)

delete_uc = DeleteVideoUseCase(
Expand Down
Loading
Loading