Time based index deletion on Elasticsearch

A estas alturas, nadie se sorprende si afirmamos que Elasticsearch es una de las bases de datos distribuidas orientadas a documentos mas populares del mercado, gracias a su escalabilidad horizontal, bajos tiempos de respuesta y su potente motor de búsquedas de cadenas de texto en tiempo real, al que da vida Apache Lucene.

Su uso como sistema de observabilidad en el que almacenar y explotar logs y métricas de otros sistemas, está mas que extendido entre las grandes corporaciones, pero, tarde o temprano, todas ellas se enfrentan a la misma problemática: eliminar de forma periódica aquellos datos o índices que dejen de tener valor con el paso tiempo.

Es por lo que en el presente artículo se pretende describir, algunas de las opciones disponibles para poder eliminar dichos índices de forma automatizada, en base al tiempo de vida deseado.

Caso de uso

Se dispone de una cluster “vanilla” de Elasticsearch, en que se almacenan los logs de los distintos aplicativos de la organización, con una política de rotación de índices diaria, los cuales se quieren eliminar una vez transcurridas 4 semanas.

ILM

Desde la reciente versión 7.6, Elasticsearch incorpora en su archiconocido paquete de utilidades X-Pack, la posibilidad de configurar políticas de administración del ciclo de vida de los índices (ILM), en base a requisitos de rendimiento, resistencia y retención. Tal y como señalan en la documentación oficial, esto permite automatizar tareas como:

  • La creación de un nuevo índice cada vez que el anterior alcance un cierto tamaño o número de documentos.
  • La creación y rotación de índices diarios, semanales o mensuales, archivando los anteriores.
  • El purgado de índices obsoletos, una vez que cumplen con los requisitos de retención de datos configurados.

Esto encaja a la perfección con el caso de uso descrito anteriormente, en el que se requiere una rotación diaria para los índices, así como su posterior borrado, una vez transcurrido el periodo tiempo definido.

Para ello, el primer paso es hacer uso de la API de Elasticsearch para crear una política ILM.

PUT _ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_age": "1d"
            "max_size": "25GB" 
          }
        }
      },
      "delete": {
        "min_age": "28d",
        "actions": {
          "delete": {} 
        }
      }
    }
  }
}

En esta timeseries policy se definen dos fases distintas:

  • Una primera fase en la que se especifica la política de rotación. Una vez que el índice ha sido rotado, pasa a funcionar únicamente en modo lectura.
  • Una segunda fase de borrado en la que se especifica la política de purgado, tomando como referencia la fecha de rotación, en lugar de la de creación.

Finalmente solo queda asociar la política creada a una plantilla o index-template, como la siguiente:

PUT _template/logs_template
{
  "index_patterns": ["logs-*"],                 
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "index.lifecycle.name": "logs_policy",      
    "index.lifecycle.rollover_alias": "logs"    
  }
}

Por desgracia, estas características solo están disponibles desde la versión 7.6 de Elasticsearch y para aquellos clusters que dispongan de X-Pack instalado. Es por ello que a continuación se describe una alternativa basada en AWS Lambda, que también es compatible con cualquier otro servicio FaaS del mercado.

AWS Lambda

Programar la ejecución diaria de una función Lambda de AWS para el purgado de los índices de Elasticsearch, es una opción mas que recomendable por lo siguientes motivos:

  • Su coste es irrisorio. A modo de referencia, 3 millones de ejecuciones con 512MB de RAM ascienden a 18,34$ al mes. Además, se dispone un millón de solicitudes gratuitas al mes, así como 400 000 GB/segundos de tiempo de cómputo al mes.
  • El coldstart de las funciones no es impedimento, pues no se trata de un proceso en tiempo real en el que el cliente aguarda bloqueado una respuesta.
  • La interacción con Elasticsearch se realiza mediante el protocolo HTTP, lo que resuelve todos los problemas relacionados con la gestión de conexiones de la base de datos que fueron detallados en el articulo titulado FaaS: Managing database connections.

Aunque es posible ejecutar el popular Apache Curator en una función Lambda, por preferencias de un servidor, se ha optado por desarrollar una sencilla función en NodeJS.

Dicho esto, el proceso se divide en tres sencillos pasos:

  1. En primer lugar, se recogen los índices que coincidan con el nombre especificado.
  2. Posteriormente se filtran aquellos que su fecha de creación es mayor al periodo de tiempo indicado.
  3. Finalmente, se ejecuta la operación de borrado sobre Elasticsearch para los índices filtrados.
const {
    Client
} = require('@elastic/elasticsearch');
const client = new Client({
    node: process.env.ELASTICSEARCH_ENDPOINT
});

const moment = require('moment');

const winston = require('winston');
const logger = winston.createLogger({
    level: process.env.LOGGING_LEVEL ? process.env.LOGGING_LEVEL : 'info',
    transports: [
        new winston.transports.Console()
    ]
});

exports.handler = async (event) => {
    var indices = await getIndices(event.indexPattern);
    indices = await filterIndices(indices, event.indexPattern, event.timeUnit, event.timeUnitCount);
    await deleteIndices(indices);
}

async function getIndices(indexPattern) {
    try {
        logger.info(`Retrieving Elasticsearch indices for '${indexPattern}' index pattern`);
        const result = await client.cat.indices({
            index: indexPattern,
            format: 'json'
        }, {
            ignore: [404],
            maxRetries: 3
        });
        const indices = [];
        result.body.forEach(indexInformation => indices.push(indexInformation.index));
        if (indices.length > 0) {
            logger.info(`Retrieved indices are ${indices}`);
        } else {
            logger.info(`No index found for provided index pattern`);
        }
        return indices;
    } catch (error) {
        logger.error(`An error ocurred retrieving Elasticsearch indices`, error);
        throw (error);
    }
}

async function filterIndices(indices, indexPattern, unit, unitCount) {
    try {
        if (indices.length > 0) {
            logger.info(`Filtering indices older than ${unitCount} ${unit}`);
            indices = indices.filter(indice => moment().diff(new Date(indice.replace(indexPattern.replace('*', ''), '')), unit) >= unitCount);
            if (indices.length > 0) {
                logger.info(`Filtered indices are ${indices}`);
            } else {
                logger.info(`No index found for the provided time frame`);
            }
        }
        return indices;
    } catch (error) {
        logger.error(`An error ocurred filtering indices`, error);
        throw (error);
    }
}

async function deleteIndices(indices) {
    try {
        if (indices.length > 0) {
            logger.info(`Deleting ${indices} indices`);
            const result = await client.indices.delete({
                index: indices,
                format: 'json'
            }, {
                ignore: [404],
                maxRetries: 3
            });
            logger.info(`${indices} indices have been deleted`);
        } else {
            logger.info(`There is no index to delete`);
        }
    } catch (error) {
        logger.error(`An error ocurred deleting indices`, error);
        throw (error);
    }
}

Para programar su despliegue y ejecución, se ha optado por hacer uso del serverless framework, el cual permite definir todos los parámetros necesarios a través del fichero serverless.yaml

service: aws-lambda-elasticsearch-time-based-index-deletion

provider:
  name: aws
  runtime: nodejs12.x
  timeout: 120
  region: ${env:AWS_REGION}
  memorySize: 256
  iamRoleStatements:
    - Effect: Allow
      Action:
        - lambda:InvokeFunction
      Resource: "*"

functions:
  awslambdaelasticsearchtimebasedindexdeletion:
    handler: index.handler
    name: aws-lambda-elasticsearch-time-based-index-deletion
    environment:
      ELASTICSEARCH_ENDPOINT: http://my-elasticsearch-endpoint:9200
      LOGGING_LEVEL: info
    events:
      - schedule: 
          name: logs-index-deletion
          description: 'Removes logs indices older than 4 weeks at 00:00 each day.' 
          rate: cron(0 0 * * *)
          input:
            indexPattern: 'logs-*'
            timeUnit: 'weeks'
            timeUnitCount: 4

Conclusiones

En conclusión, existen diversas alternativas para eliminar de forma periódica aquellos datos o índices que dejen de tener valor con el paso tiempo. En el presente artículo se han desgranado dos de ellas, si bien no son las únicas.

Por un lado, ILM permite implementarlo directamente a nivel de producto, lo cual simplifica la gestión y mejora el rendimiento, si bien no todas las organizaciones disponen de un Elasticsearch 7.6 o superior con X-Pack instalado. Es por ello qué las funciones AWS Lambda (o similares) cobran especial interés, sobretodo sí se tiene en cuenta su bajo coste.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s