본문 바로가기
삵
Miscellaneous

ILM으로 로그 인덱스 수명 관리하기

나나크나로·2026년 10월 10일·조회 1

로그를 한 인덱스에 계속 밀어 넣다 보면 어느 날 디스크 경보가 울린다. 처음엔 logstash-2026.10처럼 월 단위로 끊어 쓰다가, 트래픽이 늘면 그마저 수백 GB짜리 거대 인덱스가 되고, 오래된 데이터는 지우지도 못한 채 비싼 SSD만 잡아먹는다. 수동으로 curator 크론을 돌려 본 사람도 많겠지만, 요즘은 클러스터가 알아서 끊고 옮기고 지우게 만드는 쪽이 운영 부담이 적다.

결론부터 말하자면, Elasticsearch는 ILM(Index Lifecycle Management), OpenSearch는 ISM(Index State Management)으로 인덱스 수명을 자동화한다. 롤오버로 인덱스를 일정 크기나 나이에서 끊고, hot에서 warm 노드로 옮긴 뒤, 보관 기한이 지나면 삭제한다. 두 제품은 2021년 분기 이후 API가 갈라져 정책 JSON과 엔드포인트가 다르므로, 쓰는 제품에 맞춰 골라야 한다.

1. 개념 먼저: 롤오버와 alias, hot-warm

롤오버(rollover)는 현재 쓰기 인덱스가 정한 조건(크기, 나이, 문서 수)에 도달하면 번호를 하나 올린 새 인덱스를 만들어 그쪽으로 쓰기를 넘기는 동작이다. logs-000001이 꽉 차면 logs-000002가 생기고, 애플리케이션은 인덱스 이름이 아니라 쓰기 alias(예: logs) 하나만 바라본다.

alias는 여러 실제 인덱스에 붙이는 별명이다. 롤오버가 일어나도 애플리케이션이 바라보는 이름은 그대로라, 색인 코드를 건드릴 필요가 없다. 이 구조가 전제이므로 애플리케이션은 반드시 alias에 써야 한다. 실제 인덱스 이름에 직접 쓰면 롤오버가 영원히 안 일어난다.

hot-warm은 노드를 성격별로 나누는 구성이다. hot 노드는 빠른 디스크로 최근 로그의 색인과 검색을 받고, warm 노드는 느리고 싼 디스크로 조회 빈도가 낮은 과거 로그를 보관한다. 정책이 인덱스를 나이에 따라 hot에서 warm으로 재배치하면, 비싼 저장소를 최근 데이터에만 쓸 수 있다.

2. Elasticsearch ILM 정책 정의

ILM은 인덱스를 hot, warm, cold, frozen, delete 다섯 단계(phase)로 넘긴다. 로그라면 hot에서 롤오버, warm에서 재배치, delete에서 삭제 정도면 충분하다. _ilm/policy API로 정책을 만든다.

PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50gb",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "2d",
        "actions": {
          "set_priority": { "priority": 50 }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": { "delete": {} }
      }
    }
  }
}

hot 단계의 rollover는 max_primary_shard_size(가장 큰 주 샤드가 50GB 도달)나 max_age(인덱스 생성 1일 경과) 중 먼저 걸리는 조건에서 끊는다. 다른 조건으로 max_docs, max_primary_shard_docs가 있다. max_size는 9.3부터 비권장이므로 max_primary_shard_size를 쓴다.

warm 단계의 min_age는 롤오버 시점부터 센 나이다. 데이터 티어를 쓰는 클러스터라면 warm 단계에 진입할 때 ILM이 암묵적 migrate 동작으로 인덱스를 data_warm 노드로 옮긴다. 커스텀 노드 속성으로 직접 지정하려면 allocate 동작에 require를 넣는다. delete 단계는 롤오버 후 30일이 지나면 인덱스를 지운다.

3. 인덱스 템플릿과 부트스트랩 인덱스 연결

정책만 만들면 아무 일도 안 일어난다. 인덱스에 정책과 쓰기 alias 이름을 심어 줘야 한다. 인덱스 템플릿의 설정에 넣는다.

PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-policy",
      "index.lifecycle.rollover_alias": "logs"
    }
  }
}

그다음 첫 인덱스를 손으로 한 번 만들고, 거기에 쓰기 alias를 붙인다. 이걸 부트스트랩 인덱스라고 한다.

PUT logs-000001
{
  "aliases": {
    "logs": { "is_write_index": true }
  }
}

인덱스 이름은 반드시 -000001처럼 숫자로 끝나야 한다(정규식 ^.*-\d+$). 롤오버가 이 숫자를 하나 올려 다음 이름을 짓기 때문이다. is_write_index: true로 지정한 인덱스가 현재 쓰기 대상이고, 롤오버가 일어나면 이 플래그가 새 인덱스로 넘어간다.

4. 롤오버 동작 확인

이제 alias로 로그를 색인한다.

POST logs/_doc
{ "@timestamp": "2026-10-10T09:00:00Z", "msg": "hello" }

정책이 잘 붙었는지, 지금 어느 단계인지는 _ilm/explain으로 본다.

GET logs/_ilm/explain
{
  "indices": {
    "logs-000001": {
      "managed": true,
      "policy": "logs-policy",
      "phase": "hot",
      "action": "rollover",
      "step": "check-rollover-ready"
    }
  }
}

ILM은 폴링 주기마다 조건을 확인한다. 기본 주기는 indices.lifecycle.poll_interval로 10분이라, 조건을 채워도 롤오버가 즉시 안 일어난다. 테스트할 때는 이 값을 짧게 줄여 두면 기다리는 시간이 준다.

PUT _cluster/settings
{ "transient": { "indices.lifecycle.poll_interval": "10s" } }

롤오버가 돌면 logs-000002가 생기고 GET logs/_alias에서 쓰기 플래그가 새 인덱스로 옮겨간 것을 확인할 수 있다.

5. OpenSearch ISM 정책 정의

OpenSearch는 phase 대신 state(상태) 기반이다. 각 상태는 실행할 actions와, 다음 상태로 넘어갈 transitions를 가진다. _plugins/_ism/policies API로 만든다.

PUT _plugins/_ism/policies/logs-policy
{
  "policy": {
    "description": "롤오버 후 30일 뒤 삭제",
    "default_state": "hot",
    "states": [
      {
        "name": "hot",
        "actions": [
          { "rollover": { "min_primary_shard_size": "50gb", "min_index_age": "1d" } }
        ],
        "transitions": [
          { "state_name": "warm", "conditions": { "min_index_age": "2d" } }
        ]
      },
      {
        "name": "warm",
        "actions": [
          { "allocation": { "require": { "box_type": "warm" } } },
          { "index_priority": { "priority": 50 } }
        ],
        "transitions": [
          { "state_name": "delete", "conditions": { "min_index_age": "30d" } }
        ]
      },
      {
        "name": "delete",
        "actions": [ { "delete": {} } ],
        "transitions": []
      }
    ],
    "ism_template": [
      { "index_patterns": ["logs-*"], "priority": 100 }
    ]
  }
}

ISM의 롤오버 조건은 min_ 접두어를 쓴다. min_size, min_primary_shard_size, min_index_age, min_doc_count 중 하나라도 채우면 롤오버한다. allocation 동작의 require.box_type은 warm 노드에 붙인 커스텀 속성 이름이다. 노드 시작 설정에 node.attr.box_type: warm 같은 속성을 미리 심어 둬야 재배치가 먹는다.

맨 아래 ism_template이 ILM의 인덱스 템플릿 설정을 대신한다. logs-* 패턴으로 만들어지는 인덱스에 이 정책이 자동으로 붙는다. 단, 정책이 존재한 뒤 생성된 인덱스에만 적용된다. 이미 있는 인덱스에는 POST _plugins/_ism/add/인덱스이름으로 직접 붙인다.

6. ISM의 alias 설정과 상태 확인

OpenSearch에서 롤오버 alias는 인덱스 설정 이름이 다르다. 인덱스 템플릿에 plugins.index_state_management.rollover_alias로 지정한다.

PUT _index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "plugins.index_state_management.rollover_alias": "logs"
    }
  }
}

부트스트랩 인덱스와 쓰기 alias를 만드는 방식은 Elasticsearch와 같다(logs-000001 + is_write_index: true). 현재 상태는 explain으로 본다.

GET _plugins/_ism/explain/logs-000001

출력의 state, action, step.name을 보면 지금 hot에 있는지, 롤오버를 기다리는지, 실패했는지 알 수 있다. ISM 작업 주기는 plugins.index_state_management.job_interval(기본 5분)로 돌기 때문에 이쪽도 반영이 즉시는 아니다.

7. 자주 걸리는 함정

현장에서 반복해서 만나는 곳을 짚는다.

  • 실제 인덱스에 직접 색인: alias가 아니라 logs-000001에 바로 쓰면 롤오버가 안 일어난다. 색인 대상은 항상 alias여야 한다.
  • 이름이 숫자로 안 끝남: logs, logs-app처럼 숫자 꼬리가 없으면 롤오버가 다음 이름을 못 만들어 실패한다. explain에서 step이 멈춰 있으면 이 경우를 의심한다.
  • 정책보다 먼저 만든 인덱스: ISM의 ism_template, ILM의 인덱스 템플릿 모두 이후 생성분에만 적용된다. 기존 인덱스는 수동으로 붙인다.
  • box_type 속성 누락: ISM allocation이나 ILM allocate로 warm 재배치를 걸었는데 그런 속성의 노드가 없으면 샤드가 배치 불가 상태로 걸린다. 노드 속성을 먼저 확인한다.
  • ILM과 ISM 정책을 혼동: 두 제품의 JSON 구조와 엔드포인트가 다르다. ES 정책을 OpenSearch에 그대로 넣으면 안 먹는다.

8. 정리

로그 인덱스는 롤오버로 적당한 크기에서 끊고, hot에서 warm으로 옮기고, 기한이 지나면 지우는 세 동작이 자동으로 돌면 디스크 걱정이 크게 준다. Elasticsearch면 ILM phase와 _ilm/policy, _ilm/explain을, OpenSearch면 ISM state와 _plugins/_ism/policies, _plugins/_ism/explain을 쓴다. 롤오버 조건은 시간 단독보다 max_primary_shard_size(ES) 또는 min_primary_shard_size(OpenSearch)로 샤드 크기를 같이 걸어 샤드가 과하게 커지지 않게 하는 쪽을 권한다.

자주 묻는 질문

ILM과 ISM은 같은 것인가?

역할은 같지만 다른 구현이다. ILM은 Elasticsearch, ISM은 OpenSearch의 인덱스 수명 자동화 기능이다. ILM은 hot/warm/cold/frozen/delete 단계(phase) 기반이고 _ilm/policy로 정의한다. ISM은 상태(state)와 전이(transition) 기반이며 _plugins/_ism/policies로 정의한다. 정책 JSON 구조와 API 엔드포인트가 달라 서로 호환되지 않는다.

롤오버가 조건을 채웠는데도 안 일어난다. 왜인가?

먼저 색인을 실제 인덱스 이름이 아니라 쓰기 alias에 하고 있는지 확인한다. 다음으로 인덱스 이름이 -000001처럼 숫자로 끝나는지 본다. 둘 다 맞으면 폴링 주기 때문일 수 있다. ILM은 기본 10분, ISM은 기본 5분 주기로 조건을 확인하므로 즉시 반영되지 않는다. explain API로 현재 step과 에러 메시지를 확인한다.

롤오버 조건은 크기와 나이 중 무엇으로 거는 게 좋은가?

샤드 크기 조건을 기본으로 걸고 나이를 보조로 거는 구성을 권한다. Elasticsearch는 max_primary_shard_size, OpenSearch는 min_primary_shard_size로 가장 큰 주 샤드 크기를 제한한다. 시간만으로 끊으면 트래픽이 몰린 날 하나의 인덱스가 과도하게 커질 수 있다. 샤드 크기는 대략 수십 GB 수준을 기준으로 잡고 환경에 맞춰 조정한다.

이미 운영 중인 인덱스에도 정책을 적용할 수 있나?

적용된다. 다만 템플릿 기반 자동 연결(ILM의 인덱스 템플릿, ISM의 ism_template)은 정책 생성 이후 만들어진 인덱스에만 붙는다. 기존 인덱스는 수동으로 연결한다. OpenSearch는 POST _plugins/_ism/add/인덱스이름으로, Elasticsearch는 해당 인덱스 설정에 index.lifecycle.name을 지정해 붙인다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.