From 05dbb4c8e5751666b471efa79fa5bc07b980ca65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9E=AC=EC=A4=91?= <126754298+m-a-king@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:27:56 +0900 Subject: [PATCH] =?UTF-8?q?infra:=20prod=20=EB=B0=95=EC=8A=A4=20=EA=B4=80?= =?UTF-8?q?=EC=B8=A1=20=EB=B0=B0=EC=84=A0=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=20-=20Alloy=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EB=B9=84=EC=A0=80=EB=8B=9D=20+=20=EC=95=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=8F=99=20SSOT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - infra/scripts/provision-observability.sh: TeamPiKi/infra 공통 Alloy 블록을 fetch 하고, Grafana Cloud 자격을 SSM Parameter Store 에서 읽어 extractor 값 (environment=prod, box=piki-extractor)으로 공통 블록을 호출한다. --ref 로 infra PR 머지 전 브랜치 선검증 가능. - infra/scripts/run-extractor.sh: 지금까지 임시(수동 SSM)로 기동되던 앱 컨테이너의 run 파라미터를 이 스크립트 한 곳으로 흡수한다 - docker label opt-in(관측 대상 선언)과 트레이스 export env 를 신설 배선한다. - README.md: 운영(prod 박스) 섹션 추가 - terraform/기동/관측 스크립트 좌표 안내. 앱 코드 변경 없음 - TRACING_OTLP_ENABLED/OTLP_ENDPOINT env 배선은 application.yml 에 이미 있다. --- README.md | 8 +++ infra/scripts/provision-observability.sh | 90 +++++++++++++++++++++++ infra/scripts/run-extractor.sh | 91 ++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100755 infra/scripts/provision-observability.sh create mode 100755 infra/scripts/run-extractor.sh diff --git a/README.md b/README.md index b95f6f5..c57588e 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,11 @@ PIKI 의 상품 추출 서비스. 상품 URL(또는 S3 이미지)을 받아 fetc ./gradlew test # Docker 불필요 (DB 없음) ./gradlew bootRun # 기본 포트 8090 ``` + +## 운영 (prod 박스) + +- 박스 자체: `infra/extractor/` (terraform, EC2·SG·IAM). +- 앱 컨테이너 기동: `infra/scripts/run-extractor.sh` — 컨테이너 (재)기동의 SSOT. +- 관측(Alloy) 배선: `infra/scripts/provision-observability.sh` — 공통 Alloy 블록을 이 박스 값으로 호출. +- 둘 다 박스 안에서 SSM(run-command 또는 세션)으로 실행하며, 시크릿은 SSM Parameter Store(`/piki-extractor/*`)에서 읽는다. +- 관측 계약(라벨 opt-in 등)은 TeamPiKi/infra 의 `contracts/observability.md` 가 SSOT. diff --git a/infra/scripts/provision-observability.sh b/infra/scripts/provision-observability.sh new file mode 100755 index 0000000..f631611 --- /dev/null +++ b/infra/scripts/provision-observability.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# +# extractor prod 박스 관측(Alloy) 배선 - 이 스크립트가 값의 SSOT다. +# +# TeamPiKi/infra 의 공통 Alloy 블록(blocks/alloy/config.alloy + provision-alloy.sh)을 +# fetch 하고, Grafana Cloud 자격을 SSM Parameter Store 에서 읽어 env 로 얹은 뒤 +# extractor 의 값(environment=prod, box=piki-extractor)으로 공통 블록을 호출한다. +# +# 실행 위치: extractor 박스 안 (SSM run-command 또는 세션 접속 후 직접 실행). +# 이 박스에는 aws cli 가 없어 SSM 조회는 dockerized aws cli 로 한다 +# (인스턴스 IAM 롤 + IMDSv2 hop limit 2 라 컨테이너 안에서도 자격 획득 가능). +# +# 전제: TeamPiKi/infra 의 공통 Alloy 블록 PR 이 머지돼 있어야 한다. 머지 전에는 +# --ref 로 그 PR 의 브랜치명을 지정해 선검증할 수 있다. +# +# 사용 예: +# provision-observability.sh # main(머지된 공통 블록) 사용 +# provision-observability.sh --ref feat/alloy-common-block # 머지 전 브랜치로 선검증 +# +# 인자: +# --ref (선택) TeamPiKi/infra git ref. 기본 main +# +# 종료 코드: 성공 0, fetch/SSM 조회/공통 블록 실행 실패 1, 인자 오류 2 + +set -euo pipefail + +REF="main" + +while [ $# -gt 0 ]; do + case "$1" in + --ref) REF="${2:-}"; shift 2;; + *) echo "unknown arg: $1" >&2; exit 2;; + esac +done + +[ -n "$REF" ] || { echo "--ref 값이 비었다" >&2; exit 2; } + +WORK_DIR="/tmp/piki-obs" +mkdir -p "$WORK_DIR" + +INFRA_RAW_BASE="https://raw.githubusercontent.com/TeamPiKi/infra/$REF/blocks/alloy" + +# --- 1. 공통 Alloy 블록 fetch (repo public, 인증 불필요) --- +curl -fsSL "$INFRA_RAW_BASE/config.alloy" -o "$WORK_DIR/config.alloy" \ + || { echo "config.alloy fetch 실패 (ref=$REF)" >&2; exit 1; } +curl -fsSL "$INFRA_RAW_BASE/provision-alloy.sh" -o "$WORK_DIR/provision-alloy.sh" \ + || { echo "provision-alloy.sh fetch 실패 (ref=$REF)" >&2; exit 1; } + +# --- 2. Grafana Cloud 자격을 SSM Parameter Store 에서 조회 --- +# 박스엔 aws cli 가 없어 dockerized aws cli 로 조회한다. +get_ssm_param() { + local param_name="$1" + docker run --rm --network host \ + -e AWS_DEFAULT_REGION=ap-northeast-2 \ + amazon/aws-cli ssm get-parameter \ + --name "$param_name" \ + --with-decryption \ + --query 'Parameter.Value' \ + --output text +} + +# metrics/logs/token 5종은 필수 - 없으면 관측이 성립하지 않으므로 즉시 실패. +GRAFANA_METRICS_URL=$(get_ssm_param /piki-extractor/grafana-metrics-url) \ + || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/grafana-metrics-url" >&2; exit 1; } +GRAFANA_METRICS_USER=$(get_ssm_param /piki-extractor/grafana-metrics-user) \ + || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/grafana-metrics-user" >&2; exit 1; } +GRAFANA_LOGS_URL=$(get_ssm_param /piki-extractor/grafana-logs-url) \ + || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/grafana-logs-url" >&2; exit 1; } +GRAFANA_LOGS_USER=$(get_ssm_param /piki-extractor/grafana-logs-user) \ + || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/grafana-logs-user" >&2; exit 1; } +GRAFANA_CLOUD_TOKEN=$(get_ssm_param /piki-extractor/grafana-cloud-token) \ + || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/grafana-cloud-token" >&2; exit 1; } + +# traces 2종은 선택 - 없으면 빈 값으로 둔다. 공통 config.alloy 가 더미 endpoint 로 +# fallback 해 trace export 만 비활성화되고(metrics/logs 는 정상 동작), 스크립트는 실패하지 않는다. +GRAFANA_TRACES_URL=$(get_ssm_param /piki-extractor/grafana-traces-url) || GRAFANA_TRACES_URL="" +GRAFANA_TRACES_USER=$(get_ssm_param /piki-extractor/grafana-traces-user) || GRAFANA_TRACES_USER="" + +export GRAFANA_METRICS_URL GRAFANA_METRICS_USER +export GRAFANA_LOGS_URL GRAFANA_LOGS_USER +export GRAFANA_TRACES_URL GRAFANA_TRACES_USER +export GRAFANA_CLOUD_TOKEN + +# --- 3. 공통 블록 호출 - 이 박스는 prod 전용이라 environment=prod 고정 --- +# (dev extractor 는 core dev 박스에 동거하며 그쪽 배선은 core 소관, 여기서 다루지 않는다) +bash "$WORK_DIR/provision-alloy.sh" \ + --config "$WORK_DIR/config.alloy" \ + --name piki-alloy \ + --environment prod \ + --box piki-extractor diff --git a/infra/scripts/run-extractor.sh b/infra/scripts/run-extractor.sh new file mode 100755 index 0000000..7a8d2c8 --- /dev/null +++ b/infra/scripts/run-extractor.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# +# extractor 앱 컨테이너 (재)기동 - 이 스크립트가 컨테이너 기동의 신설 SSOT다. +# +# 지금까지 앱 컨테이너(piki-extractor)는 임시(수동 SSM)로 기동돼 run 파라미터가 +# repo 어디에도 없었다. 라벨·env 는 이 스크립트 한 곳에만 박는다 - 이 스크립트 +# 밖에서 env 를 더하지 않는다. +# +# 실행 위치: extractor 박스 안 (SSM run-command 또는 세션 접속 후 직접 실행). +# 박스엔 aws cli 가 없어 SSM 조회는 dockerized aws cli 로 한다 +# (인스턴스 IAM 롤 + IMDSv2 hop limit 2 라 컨테이너 안에서도 자격 획득 가능). +# +# 사용 예: +# run-extractor.sh --image piki-extractor:latest +# +# 인자: +# --image (필수) 기동할 이미지:태그. default 없음 +# +# 종료 코드: 성공(헬스체크 통과) 0, SSM 조회/헬스체크 실패 1, 인자 오류 2 + +set -euo pipefail + +IMAGE="" + +while [ $# -gt 0 ]; do + case "$1" in + --image) IMAGE="${2:-}"; shift 2;; + *) echo "unknown arg: $1" >&2; exit 2;; + esac +done + +[ -n "$IMAGE" ] || { echo "--image is required" >&2; exit 2; } + +# --- 1. GEMINI_API_KEY 를 SSM Parameter Store 에서 조회 --- +GEMINI_API_KEY=$( + docker run --rm --network host \ + -e AWS_DEFAULT_REGION=ap-northeast-2 \ + amazon/aws-cli ssm get-parameter \ + --name /piki-extractor/gemini-api-key \ + --with-decryption \ + --query 'Parameter.Value' \ + --output text +) || { echo "필수 SSM 파라미터 조회 실패: /piki-extractor/gemini-api-key" >&2; exit 1; } + +# --- 2. 기존 컨테이너 제거 후 재기동 (없으면 무시) --- +docker rm -f piki-extractor >/dev/null 2>&1 || true + +docker run -d --name piki-extractor --restart unless-stopped \ + -p 8090:8090 \ + --add-host=host.docker.internal:host-gateway \ + --label piki.observe=true \ + --label piki.service=piki-extractor \ + --label piki.metrics.port=8090 \ + --label piki.metrics.path=/actuator/prometheus \ + -e AWS_REGION=ap-northeast-2 \ + -e GEMINI_API_KEY="$GEMINI_API_KEY" \ + -e TRACING_OTLP_ENABLED=true \ + -e OTLP_ENDPOINT=http://host.docker.internal:4318/v1/traces \ + -e LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs \ + "$IMAGE" + +# 라벨 4종(piki.observe/piki.service/piki.metrics.port/piki.metrics.path)은 공통 Alloy 의 +# label opt-in 계약(TeamPiKi/infra contracts/observability.md) - 이 라벨이 있어야 박스 +# Alloy 가 이 컨테이너를 스크레이프 대상으로 인식한다. piki.metrics.port 는 컨테이너 +# 내부 포트가 아니라 "호스트 127.0.0.1 에서 닿는 포트" 다. +# +# -p 8090:8090 전체 바인드: 이 박스는 SG 로 인바운드가 앱 SG 에서만 허용되니(SG 격리) +# 외부 노출이 없다. core 앱은 이 박스의 private IP 로 이 포트를 호출한다. +# +# --add-host=host.docker.internal:host-gateway + OTLP_ENDPOINT=...host.docker.internal:4318: +# 트레이스는 앱이 박스 로컬 Alloy(4318, OTLP HTTP)로 능동적으로 push 하는 유일한 신호다. +# 이 컨테이너는 bridge 네트워크라 host-gateway 를 거쳐야 호스트의 Alloy 에 닿는다. +# +# LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs: 공통 Loki 파이프라인이 ECS JSON 포맷을 전제로 +# level 라벨링·trace 상관을 한다(Spring Boot 구조화 로깅, relaxed binding 으로 이 env 하나면 +# 활성화된다). 형식이 다르면 그쪽 파이프라인이 파싱하지 못한다. +# +# 이 스크립트를 박스에 첫 적용할 때는 기존 임시 기동 컨테이너를 +# `docker inspect piki-extractor` 로 대조해, 누락된 env 가 있으면 여기(이 스크립트)에 +# 흡수시킨다 - 이 스크립트 밖에서 별도로 env 를 더하지 않는다. + +# --- 3. 헬스 대기 - 공통 healthcheck 블록을 dogfooding --- +HEALTHCHECK_SH="/tmp/piki-run-extractor-healthcheck.sh" +curl -fsSL https://raw.githubusercontent.com/TeamPiKi/infra/main/blocks/healthcheck.sh -o "$HEALTHCHECK_SH" \ + || { echo "healthcheck.sh fetch 실패" >&2; exit 1; } + +bash "$HEALTHCHECK_SH" \ + --url http://localhost:8090/actuator/health \ + --interval 5 \ + --attempts 24 \ + --expect-body '"status":"UP"'