데이터 보존 | Splunk Indexer Cluster에서 coldToFrozenScript로 S3 아카이빙 구성하기 (1/2)
본문 바로가기

Splunk/Splunk Project

데이터 보존 | Splunk Indexer Cluster에서 coldToFrozenScript로 S3 아카이빙 구성하기 (1/2)

728x90
반응형

Splunk를 운영하다 보면 indexed data의 보관 기간과 디스크 용량을 함께 고려해야 한다. 최근 데이터는 Splunk local disk에 유지하는 것이 적절하지만, 장기 보관 목적의 오래된 로그까지 계속 local disk에 두면 storage 비용과 운영 부담이 커진다.

이 글에서는 Splunk Indexer Cluster 환경에서 cold bucket이 frozen으로 전환되는 시점에 AWS S3로 아카이빙하는 구성을 정리한다.

Splunk Enterprise Indexer Cluster
Replication Factor = 2
Search Factor      = 2
Archive Storage    = AWS S3
Archive Method     = coldToFrozenScript

1. Splunk Bucket Lifecycle

Splunk index data는 bucket 단위로 저장된다. 일반적인 bucket lifecycle은 다음과 같다.

hot → warm → cold → frozen

 

단계 기본 위치 검색 가능 여부 설명
hot db 가능 현재 write 중인 bucket
warm db 가능 hot에서 roll된 bucket
cold colddb 가능 오래된 검색 가능 bucket
frozen archive 또는 삭제 기본적으로 검색 불가 retention 초과 후 제거되는 단계

 

중요한 점은 cold bucket이라고 해서 바로 S3로 가는 것이 아니라는 점이다.

coldToFrozenScript는 cold bucket이 frozen으로 전환되는 순간 실행된다.

cold bucket 전체 X
frozen 조건에 도달한 cold bucket O

2. 기존 indexes.conf 구성

이번 환경에서는 volume을 다음과 같이 구성했다.

[volume:primary]
path = /opt/splunk/var/lib/splunk
maxVolumeDataSizeMB = 3300000

[volume:summary]
path = /opt/splunk/var/lib/splunk/_summaries
maxVolumeDataSizeMB = 250000

[default]
homePath = volume:primary/$_index_name/db
coldPath = volume:primary/$_index_name/colddb
thawedPath = $SPLUNK_DB/$_index_name/thaweddb
summaryHomePath = volume:summary/$_index_name/summary
tstatsHomePath = volume:summary/$_index_name/datamodel_summary

실제 bucket 경로는 다음과 같은 형태가 된다.

hot/warm bucket:
  /opt/splunk/var/lib/splunk/<index>/db

cold bucket:
  /opt/splunk/var/lib/splunk/<index>/colddb

thawed bucket:
  /opt/splunk/var/lib/splunk/<index>/thaweddb

이번 글에서 아카이빙 대상으로 볼 bucket은 아래 경로에 있는 bucket이다. 단, 이 중에서도 frozen 조건에 도달한 bucket만 S3로 아카이빙된다.

/opt/splunk/var/lib/splunk/<index>/colddb/<bucket>

3. colddb를 그냥 S3로 복사하면 안 되는 이유

처음에는 colddb 아래 bucket을 주기적으로 S3로 복사하면 되지 않을까 생각할 수 있다.
하지만 이 방식은 운영 관점에서 권장하지 않는다.

colddb는 아직 Splunk가 검색 대상으로 관리하는 영역이다.
따라서 외부 스크립트로 colddb의 bucket을 임의로 이동하거나 삭제하면 다음 문제가 생길 수 있다.

검색 누락
bucket 불일치
indexer cluster fix-up 이슈
중복 백업
retention 정책과의 불일치

따라서 올바른 방식은 Splunk의 lifecycle에 맞춰 cold bucket이 frozen으로 전환될 때 아카이빙하는 것이다.

colddb 직접 복사 = 단순 백업에 가까움
colddb 직접 삭제 = 위험함
권장 방식 = coldToFrozenScript 사용

4. Indexer Cluster에서 db_와 rb_의 차이

Indexer Cluster 환경에서는 bucket 이름 앞에 db_ 또는 rb_가 붙는다.

db_ = originating bucket
rb_ = replicated bucket


예를 들어 RF=2, SF=2 환경에서는 하나의 logical bucket에 대해 보통 다음과 같은 copy가 존재한다.

IDX01:
/opt/splunk/var/lib/splunk/querypie/colddb/db_1760000000_1759000000_123_GUID

IDX02:
/opt/splunk/var/lib/splunk/querypie/colddb/rb_1760000000_1759000000_123_GUID


여기서 db_는 원본 bucket copy이고, rb_는 복제 bucket copy다. RF=2, SF=2에서는 두 copy가 모두 searchable copy일 수 있다. 즉, rb_라고 해서 단순히 검색 불가능한 백업본이라고 보면 안 된다.


5. RF=2, SF=2 환경에서 중복 아카이빙 문제

문제는 S3 아카이빙 시점에 발생한다. db_rb_는 같은 logical bucket의 copy인데, 이름이 다르기 때문에 스크립트에서 그대로 처리하면 서로 다른 bucket으로 인식될 수 있다.

db_1760000000_1759000000_123_GUID
rb_1760000000_1759000000_123_GUID

이 둘을 그대로 S3에 올리면 같은 데이터가 중복 저장될 수 있다. 따라서 중복 제거를 위해 rb_db_ 기준으로 정규화한다.

CANONICAL_BUCKET_NAME="${BUCKET_NAME/#rb_/db_}"

이렇게 하면 다음 두 bucket은 같은 canonical bucket name을 갖게 된다.

db_1760000000_1759000000_123_GUID
rb_1760000000_1759000000_123_GUID

정규화 결과:
db_1760000000_1759000000_123_GUID

6. S3 아카이빙 구조

S3에는 다음 구조로 저장한다.

s3://<bucket>/splunk-frozen/
├── data/
│   └── <index>/
│       └── <canonical_bucket>/
├── _locks/
│   └── <index>/
│       └── <canonical_bucket>.lock
├── _manifests/
│   └── <index>/
│       └── <canonical_bucket>.done
└── duplicates/
    └── <index>/
        └── <peer>/
            └── <canonical_bucket>/
Prefix 용도
data/ 정상 아카이빙된 frozen bucket
_locks/ 특정 bucket 업로드를 선점한 peer 표시
_manifests/ 업로드 완료 marker
duplicates/ lock은 있으나 done이 없는 비정상 상황에서 fallback 업로드


동작 흐름은 다음과 같다.

1. bucket이 frozen 전환됨
2. coldToFrozenScript 실행
3. bucket 이름을 canonical name으로 정규화
4. S3에 done marker가 있으면 skip
5. done marker가 없으면 lock 생성 시도
6. lock 생성에 성공한 peer만 S3 data prefix로 업로드
7. 업로드 완료 후 done marker 생성
8. 다른 peer는 done marker 확인 후 skip
9. lock은 있는데 done이 없으면 fallback duplicate 경로로 업로드

7. coldToFrozenScript 전문

아래 스크립트를 Cluster Manager의 app 아래에 저장한다.

/opt/splunk/etc/manager-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh

공개용 글이므로 S3 bucket 이름은 예시값으로 작성했다. 실제 환경에서는 S3_BUCKET 값을 운영 환경에 맞게 변경하면 된다.

#!/bin/bash
set -euo pipefail

###############################################################################
# Splunk coldToFrozen S3 Archive Script with Dedupe
#
# Environment:
#   - Splunk Indexer Cluster
#   - RF=2, SF=2
#
# Purpose:
#   - frozen 전환되는 cold bucket을 S3로 아카이빙
#   - db_ / rb_ 복제 bucket 중복 업로드 방지
#   - S3 lock / done marker 기반 dedupe
#   - lock 획득 peer 장애 시 fallback duplicate 경로로 데이터 유실 방지
#
# Expected bucket path:
#   /opt/splunk/var/lib/splunk/<index>/colddb/<bucket>
#
# Bucket name examples:
#   db_1760000000_1759000000_123_GUID
#   rb_1760000000_1759000000_123_GUID
#
# Dedupe rule:
#   rb_... -> db_... 로 canonical bucket name 정규화
#
# S3 structure:
#   s3://<bucket>/splunk-frozen/data/<index>/<canonical_bucket>/
#   s3://<bucket>/splunk-frozen/_locks/<index>/<canonical_bucket>.lock
#   s3://<bucket>/splunk-frozen/_manifests/<index>/<canonical_bucket>.done
#   s3://<bucket>/splunk-frozen/duplicates/<index>/<peer>/<canonical_bucket>/
###############################################################################

export AWS_DEFAULT_REGION="ap-northeast-2"

SPLUNK_HOME="${SPLUNK_HOME:-/opt/splunk}"

S3_BUCKET="my-splunk-frozen-archive-bucket"
ROOT_PREFIX="splunk-frozen"

AWS_BIN="${AWS_BIN:-/usr/bin/aws}"

if [[ ! -x "$AWS_BIN" ]]; then
  AWS_BIN="$(command -v aws || true)"
fi

if [[ -z "${AWS_BIN}" || ! -x "$AWS_BIN" ]]; then
  echo "ERROR: aws cli not found" >&2
  exit 1
fi

BUCKET_PATH="${1:?Usage: $0 <bucket_path>}"

if command -v realpath >/dev/null 2>&1; then
  BUCKET_PATH="$(realpath "$BUCKET_PATH")"
fi

BUCKET_NAME="$(basename "$BUCKET_PATH")"

# RF=2/SF=2 indexer cluster dedupe 처리
# db_ = originating bucket
# rb_ = replicated bucket
# 같은 logical bucket copy이므로 S3 dedupe key는 db_ 기준으로 정규화
CANONICAL_BUCKET_NAME="${BUCKET_NAME/#rb_/db_}"

PARENT_DIR="$(basename "$(dirname "$BUCKET_PATH")")"
INDEX_NAME="$(basename "$(dirname "$(dirname "$BUCKET_PATH")")")"
PEER_NAME="$(hostname -s)"
NOW_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

LOGFILE="${SPLUNK_HOME}/var/log/splunk/coldtofrozen_s3.log"

DATA_PREFIX="${ROOT_PREFIX}/data/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}"
LOCK_KEY="${ROOT_PREFIX}/_locks/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}.lock"
DONE_KEY="${ROOT_PREFIX}/_manifests/${INDEX_NAME}/${CANONICAL_BUCKET_NAME}.done"

FALLBACK_PREFIX="${ROOT_PREFIX}/duplicates/${INDEX_NAME}/${PEER_NAME}/${CANONICAL_BUCKET_NAME}"
FALLBACK_DONE_KEY="${ROOT_PREFIX}/_fallback/${INDEX_NAME}/${PEER_NAME}/${CANONICAL_BUCKET_NAME}.done"

TMP_META="$(mktemp /tmp/splunk_coldtofrozen_meta.XXXXXX)"
trap 'rm -f "$TMP_META"' EXIT

log() {
  local msg="$*"
  echo "$(date -Is) peer=${PEER_NAME} index=${INDEX_NAME} bucket=${BUCKET_NAME} canonical=${CANONICAL_BUCKET_NAME} ${msg}" >> "$LOGFILE"
  logger -t splunk-coldtofrozen-s3 "peer=${PEER_NAME} index=${INDEX_NAME} bucket=${BUCKET_NAME} canonical=${CANONICAL_BUCKET_NAME} ${msg}" || true
}

fail() {
  log "ERROR: $*"
  echo "ERROR: $*" >&2
  exit 1
}

bucket_size_bytes() {
  du -sb "$BUCKET_PATH" 2>/dev/null | awk '{print $1}'
}

bucket_file_count() {
  find "$BUCKET_PATH" -type f 2>/dev/null | wc -l | tr -d ' '
}

write_meta() {
  local status="$1"

  cat > "$TMP_META" <<EOF
{
  "status": "${status}",
  "index": "${INDEX_NAME}",
  "bucket": "${BUCKET_NAME}",
  "canonical_bucket": "${CANONICAL_BUCKET_NAME}",
  "bucket_path": "${BUCKET_PATH}",
  "peer": "${PEER_NAME}",
  "timestamp_utc": "${NOW_UTC}",
  "size_bytes": "$(bucket_size_bytes)",
  "file_count": "$(bucket_file_count)"
}
EOF
}

s3_head_object() {
  local key="$1"

  "$AWS_BIN" s3api head-object \
    --bucket "$S3_BUCKET" \
    --key "$key" >/dev/null 2>&1
}

s3_put_object_if_absent() {
  local key="$1"
  local body="$2"

  "$AWS_BIN" s3api put-object \
    --bucket "$S3_BUCKET" \
    --key "$key" \
    --body "$body" \
    --if-none-match "*" >/dev/null 2>&1
}

archive_to_s3() {
  local source_path="$1"
  local target_prefix="$2"

  "$AWS_BIN" s3 sync \
    "${source_path}/" \
    "s3://${S3_BUCKET}/${target_prefix}/" \
    --only-show-errors
}

###############################################################################
# Validation
###############################################################################

[[ -d "$BUCKET_PATH" ]] || fail "bucket path does not exist or is not directory: ${BUCKET_PATH}"

[[ "$PARENT_DIR" == "colddb" ]] || fail "unexpected bucket parent directory. expected=colddb actual=${PARENT_DIR} path=${BUCKET_PATH}"

[[ -n "$INDEX_NAME" ]] || fail "failed to parse index name from path: ${BUCKET_PATH}"

[[ -n "$BUCKET_NAME" ]] || fail "failed to parse bucket name from path: ${BUCKET_PATH}"

if [[ "$BUCKET_NAME" != db_* && "$BUCKET_NAME" != rb_* ]]; then
  fail "unexpected bucket name. expected db_* or rb_* actual=${BUCKET_NAME}"
fi

log "start coldToFrozen archive. path=${BUCKET_PATH}"

###############################################################################
# Step 1. 이미 아카이빙 완료된 logical bucket이면 skip
###############################################################################

if s3_head_object "$DONE_KEY"; then
  log "DONE marker already exists. skip archive. done=s3://${S3_BUCKET}/${DONE_KEY}"
  exit 0
fi

###############################################################################
# Step 2. S3 lock 생성 시도
#         lock 생성에 성공한 peer만 정상 data prefix에 업로드
###############################################################################

write_meta "LOCKED"

if s3_put_object_if_absent "$LOCK_KEY" "$TMP_META"; then
  log "lock acquired. lock=s3://${S3_BUCKET}/${LOCK_KEY}"

  # lock 획득 후에도 혹시 done marker가 생겼는지 재확인
  if s3_head_object "$DONE_KEY"; then
    log "DONE marker appeared after lock acquisition. skip archive."
    exit 0
  fi

  log "archive upload started. target=s3://${S3_BUCKET}/${DATA_PREFIX}/"

  archive_to_s3 "$BUCKET_PATH" "$DATA_PREFIX"

  write_meta "DONE"

  if s3_put_object_if_absent "$DONE_KEY" "$TMP_META"; then
    log "archive completed. done=s3://${S3_BUCKET}/${DONE_KEY}"
    exit 0
  else
    # 업로드는 완료됐지만 done marker 생성이 실패한 경우
    # 이미 다른 peer가 done을 만들었을 가능성이 있으므로 확인
    if s3_head_object "$DONE_KEY"; then
      log "archive upload completed, DONE marker already exists."
      exit 0
    fi

    fail "archive uploaded but failed to create DONE marker: s3://${S3_BUCKET}/${DONE_KEY}"
  fi

else
  log "lock already exists. another peer may be archiving this logical bucket."

  #############################################################################
  # Step 3. 다른 peer가 업로드 중이면 DONE marker 대기
  #         최대 10분 대기: 60회 x 10초
  #############################################################################

  for i in $(seq 1 60); do
    sleep 10

    if s3_head_object "$DONE_KEY"; then
      log "DONE marker found after waiting. another peer archived this bucket. skip."
      exit 0
    fi

    log "waiting for DONE marker. attempt=${i}/60"
  done

  #############################################################################
  # Step 4. lock은 있는데 DONE이 없는 비정상 상황
  #         첫 번째 peer가 업로드 중 장애났을 수 있음
  #         데이터 유실 방지를 위해 peer별 fallback duplicate 경로에 업로드
  #############################################################################

  log "DONE marker not found after timeout. fallback duplicate archive started. target=s3://${S3_BUCKET}/${FALLBACK_PREFIX}/"

  archive_to_s3 "$BUCKET_PATH" "$FALLBACK_PREFIX"

  write_meta "FALLBACK_DONE"

  s3_put_object_if_absent "$FALLBACK_DONE_KEY" "$TMP_META" || true

  log "fallback duplicate archive completed. marker=s3://${S3_BUCKET}/${FALLBACK_DONE_KEY}"
  exit 0
fi

8. 스크립트 권한 설정

Cluster Manager에서 다음 명령을 실행한다.

chown splunk:splunk /opt/splunk/etc/manager-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh
chmod 750 /opt/splunk/etc/manager-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh

9. indexes.conf에 coldToFrozenScript 설정

스크립트는 Cluster Manager의 manager-apps 아래에 저장하지만, indexer peer에 배포된 후 실제 실행 경로는 peer-apps 아래가 된다. 따라서 indexes.confcoldToFrozenScript에는 peer-apps 기준 경로를 넣어야 한다.

[querypie]
frozenTimePeriodInSecs = 31536000
coldToFrozenScript = "/bin/bash" "/opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh"

 

여러 index에 적용하려면 index별로 명시한다.

[querypie]
frozenTimePeriodInSecs = 31536000
coldToFrozenScript = "/bin/bash" "/opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh"

[trendmicro]
frozenTimePeriodInSecs = 31536000
coldToFrozenScript = "/bin/bash" "/opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh"

[zscaler]
frozenTimePeriodInSecs = 31536000
coldToFrozenScript = "/bin/bash" "/opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh"

 

[default]에 넣으면 대부분의 index에 상속될 수 있다.
운영 환경에서는 필요한 index에만 명시적으로 적용하는 것을 권장한다.


10. Cluster Bundle 배포

Cluster Manager에서 bundle을 검증하고 배포한다.

/opt/splunk/bin/splunk validate cluster-bundle --check-restart
/opt/splunk/bin/splunk apply cluster-bundle

배포 후 indexer peer에서 스크립트가 정상적으로 내려갔는지 확인한다.

ls -l /opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh

btool로 설정도 확인한다.

/opt/splunk/bin/splunk btool indexes list querypie --debug \
| egrep "frozenTimePeriodInSecs|coldToFrozenScript|homePath|coldPath|thawedPath"

예상되는 형태는 다음과 같다.

coldToFrozenScript = "/bin/bash" "/opt/splunk/etc/peer-apps/00_worxphere/bin/coldToFrozenS3_dedupe.sh"
frozenTimePeriodInSecs = 31536000
homePath = volume:primary/querypie/db
coldPath = volume:primary/querypie/colddb
thawedPath = $SPLUNK_DB/querypie/thaweddb

11. AWS CLI 및 권한 확인

Indexer peer에서 Splunk 실행 계정으로 AWS CLI 접근을 확인한다.

sudo -iu splunk

aws sts get-caller-identity

aws s3 ls s3://my-splunk-frozen-archive-bucket/

테스트 object를 업로드해볼 수도 있다.

echo test > /tmp/s3_test.txt

aws s3api put-object \
  --bucket my-splunk-frozen-archive-bucket \
  --key splunk-frozen/_test/test.txt \
  --body /tmp/s3_test.txt \
  --if-none-match "*"

같은 명령을 다시 실행했을 때 overwrite가 막히면 조건부 write가 정상적으로 동작하는 것이다.


12. 운영 확인

스크립트 로그는 다음 경로에 남도록 구성했다.

/opt/splunk/var/log/splunk/coldtofrozen_s3.log

확인은 다음처럼 한다.

tail -f /opt/splunk/var/log/splunk/coldtofrozen_s3.log

S3 업로드 결과는 다음 명령으로 확인할 수 있다.

aws s3 ls s3://my-splunk-frozen-archive-bucket/splunk-frozen/ --recursive | head -50

정상적으로 동작하면 다음과 같은 object가 생성된다.

splunk-frozen/_locks/querypie/db_1760000000_1759000000_123_GUID.lock
splunk-frozen/_manifests/querypie/db_1760000000_1759000000_123_GUID.done
splunk-frozen/data/querypie/db_1760000000_1759000000_123_GUID/...

13. 이 방식의 동작 예시

RF=2, SF=2 환경에서 다음 bucket이 있다고 가정한다.

IDX01:
db_1760000000_1759000000_123_GUID

IDX02:
rb_1760000000_1759000000_123_GUID

스크립트는 rb_db_로 정규화한다.

rb_1760000000_1759000000_123_GUID
→ db_1760000000_1759000000_123_GUID

따라서 S3 기준으로는 두 bucket이 같은 logical bucket으로 취급된다.

splunk-frozen/data/querypie/db_1760000000_1759000000_123_GUID/

동작은 다음과 같다.

db_ bucket이 먼저 frozen됨
→ lock 생성 성공
→ S3 업로드
→ done marker 생성

rb_ bucket이 나중에 frozen됨
→ canonical name 기준으로 done marker 확인
→ 이미 완료된 bucket이므로 skip

반대로 rb_가 먼저 frozen되어도 문제없다.

rb_ bucket이 먼저 frozen됨
→ canonical name은 db_ 기준으로 변환
→ lock 생성 성공
→ S3 업로드
→ done marker 생성

db_ bucket이 나중에 frozen됨
→ done marker 확인
→ skip

즉, 먼저 성공한 copy 하나만 S3에 저장된다.


14. 주의사항

1. colddb를 직접 삭제하지 않는다.
2. coldToFrozenDir와 coldToFrozenScript를 동시에 설정하지 않는다.
3. Indexer Cluster에서는 db_ / rb_ 중복을 반드시 고려한다.
4. RF=2, SF=2에서는 db_와 rb_ 둘 다 searchable copy일 수 있다.
5. S3 업로드 실패 시 데이터 유실을 막기 위해 fallback duplicate 경로를 둔다.
6. S3에 올라간 frozen bucket은 바로 Splunk에서 검색되지 않는다.
7. 검색이 필요하면 thaweddb로 복원한 뒤 rebuild 절차가 필요하다.
8. 공개 문서에는 실제 AWS Account ID, bucket name, 내부 index name을 노출하지 않는다.

특히 coldToFrozenScript는 frozen 전환 시점에 실행된다. 따라서 cold bucket 전체가 바로 S3에 올라가는 것이 아니라, retention 조건에 따라 frozen 처리되는 bucket만 아카이빙된다.


15. 마무리

이번 구성의 핵심은 단순히 colddb를 S3로 복사하는 것이 아니다. Splunk의 bucket lifecycle에 맞춰 cold bucket이 frozen으로 전환되는 시점에 S3로 아카이빙해야 한다.

또한 Indexer Cluster 환경에서는 RF에 의해 같은 logical bucket이 여러 peer에 존재할 수 있다. 따라서 db_rb_ bucket을 같은 대상으로 정규화하고, S3 lock/done marker를 이용해 중복 업로드를 방지해야 한다.

최종적으로 이 구조를 사용하면 다음 효과를 얻을 수 있다.

로컬 디스크 사용량 제어
장기 보관 로그의 S3 이관
Indexer Cluster 환경에서 중복 아카이빙 방지
업로드 장애 시 fallback 경로를 통한 데이터 유실 방지

Splunk 장기 보관 정책을 설계할 때는 단순 백업이 아니라, bucket lifecycle, RF/SF 구조, thaw 복원 절차까지 함께 고려해야 한다.


 

728x90
반응형