📑 Índice del documento
- Trabajo Práctico Nº 2 —
Parte 2
- Observabilidad — Logging centralizado con EFK (Elasticsearch + Fluent Bit + Kibana)
- Pre-requisitos
- Requisitos, consideraciones y formato de entrega
- Contenidos del programa relacionados
- Práctica
- Hit #1 — Deploy del ECK Operator + Elasticsearch single-node + Kibana
- Hit #2 — Fluent Bit como DaemonSet, pipeline al scraper
- Hit #3 — Index pattern + ILM (rollover, retention 7 días)
- Hit #4 — Cookbook KQL: 6+ queries útiles
- Hit #5 — Dashboard Kibana provisionado as-code (NDJSON + import API)
- Hit #6 — Alertas via Kibana Alerting (opcional, bonus +5 %)
- Cómo entregar
- Auto-verificación
previa a la entrega
- 1)
install.shcorre limpio en cluster vacío - 2)
Elasticsearch está
greeny Kibanaavailable - 3) Fluent Bit DaemonSet está Running en todos los nodos
- 4) Elasticsearch responde a queries via HTTP
- 5) ILM policy
scraper-logsaplicada - 6) Las 6 queries KQL del Hit #4 corren sin error
- 7) Dashboard provisionado aparece en la UI
- 8)
gitleaksno detecta el password de Elastic ni el webhook Discord
- 1)
- Common pitfalls
- Criterios de evaluación
- Material de apoyo
- Referencias y Bibliografía
Trabajo Práctico Nº 2 — Parte 2
Observabilidad — Logging centralizado con EFK (Elasticsearch + Fluent Bit + Kibana)
Fecha de Entrega: 05/05/2026
🧭 Esto es la Parte 2 de 4 sobre observabilidad.
- Parte 1 — Loki + Promtail/Alloy + Grafana
- Parte 2 (acá) — EFK = Elasticsearch + Fluent Bit + Kibana
- Parte 3 — OpenTelemetry Collector + SDK
- Parte 4 — Cierre: decisiones arquitectónicas + ADR comparativo
Esta Parte 2 NO reemplaza a Parte 1 — los dos stacks van a coexistir en el mismo cluster (namespaces distintos:
observabilitypara Loki,elasticpara EFK). Eso es a propósito: van a poder comparar latencia, footprint y ergonomía con datos reales.
🤝 Partes 1 y 2 se entregan el mismo día (05/05) en un solo push final. Pueden trabajar en paralelo: Loki es más rápido de levantar y deja datos acumulándose mientras montás EFK. Cuando llegás al cookbook de queries (Hit #4 acá), ya tenés 1-2 h de logs reales en ambos backends para comparar.
Pre-requisitos: Loki desplegado de Parte 1 (puede ser en paralelo en este mismo push). Cluster k3s con al menos 8 GB de RAM libre + 15 GB de disco — Elasticsearch es mucho más pesado que Loki, y este es el primer mensaje pedagógico de la Parte 2.
Pre-requisitos
- TP 2 · Parte 1 entregada y funcionando: Loki +
Promtail + Grafana corriendo en namespace
observability, con el scraper emitiendo JSON estructurado del Hit #3 de Parte 1. Si no levanta Loki, no se evalúa esta Parte 2 — la comparativa final (Parte 4) necesita los dos stacks vivos al mismo tiempo. - TP 1 · Parte 2 vigente: scraper como
Job/CronJobenml-scraper, conlogging_setup.pyemitiendo JSON line-delimited a stdout (Hit #5 / Hit #3 reformateado en Parte 1). - Cluster k3s/k3d con recursos extra: a diferencia de Parte 1 (~6 GB), acá necesitan 8 GB libres adicionales. Elasticsearch single-node pide 2 GB de heap como piso operativo y Kibana se suma con ~1 GB. Si están corriendo todo en una laptop, cierren Chrome.
- Disco: 15 GB libres para los PVCs de Elasticsearch (10 GB datos + 2 GB snapshots + buffers). Más que los 5 GB que pedía Loki — full-text index pesa.
- Helm 3 instalado (≥ 3.16) y
kubectl≥ 1.30. Mismo set de Parte 1. - Familiaridad con CRDs: ECK (Elastic Cloud on
Kubernetes) trabaja con custom resources (
Elasticsearch,Kibana). Si nunca instalaron un operator con CRDs, repasen el TP 0 — Operators.
Requisitos, consideraciones y formato de entrega
Aplican los mismos requisitos generales del TP 1 + Parte 1 (repo público, README por hit, video, integración con IA documentada, sin secrets commiteados) más los siguientes específicos de esta Parte 2.
Infra base obligatoria — bloqueante
🚧 Sin esto la entrega no se puede evaluar. La cátedra corre tu
helm install+kubectl apply+ abre Kibana en el browser para verificar dashboards y queries. Si el stack no levanta, no se llega a corregir nada más → nota 0. Mismo criterio que Parte 1.
Carpeta
efk/en el repo (noobservability/— ese ya está ocupado por Loki):efk/ ├── README.md ← cómo levantar el stack en una corrida ├── helm/ │ ├── eck-operator-values.yaml ← values del Helm chart del ECK Operator │ └── fluent-bit-values.yaml ← values pinneados de fluent/fluent-bit ├── manifests/ │ ├── namespace.yaml ← namespace `elastic` │ ├── elasticsearch.yaml ← CR Elasticsearch (ECK CRD) │ ├── kibana.yaml ← CR Kibana (ECK CRD) │ ├── kibana-nodeport.yaml ← Service NodePort 30001 (Kibana) │ └── ilm-policy.json ← Index Lifecycle Management policy ├── dashboards/ │ └── scraper-overview.ndjson ← dashboard exportado as-code (Hit #5) ├── queries/ │ └── kql-cookbook.md ← las 6+ queries del Hit #4 └── install.sh ← script idempotente con todos los pasosScript
efk/install.shreproducible, levanta el stack desde cero con un solo comando:cd efk && ./install.sh # Output esperado al final: # ✓ ECK Operator running (kubectl get pod -n elastic-system) # ✓ Elasticsearch green (kubectl get elasticsearch -n elastic) # ✓ Kibana available (NodePort 30001 abierto) # ✓ Fluent Bit DaemonSet ready (1 pod por nodo, status Running) # ✓ ILM policy 'scraper-logs' aplicada # ✓ Index pattern 'scraper-*' creado # ✓ Dashboard 'Scraper Overview' importado via API # → Abrir https://<node-ip>:30001 (elastic / <ver secret>)Versiones pinneadas a abril 2026:
Componente Chart / CRD Versión Repo ECK Operator elastic/eck-operator2.16.xhttps://helm.elastic.co Elasticsearch CRD elasticsearch.k8s.elastic.co/v18.17.x(gestionado por ECK Operator) Kibana CRD kibana.k8s.elastic.co/v18.17.x(gestionado por ECK Operator) Fluent Bit fluent/fluent-bit0.48.x(app3.2.x)https://fluent.github.io/helm-charts ⚠️ NO usen Fluentd (el agente original del acrónimo “EFK”). Fluent Bit es el sucesor de Fluentd para el caso de agente por nodo: ~10× menos memoria (40 MB resident vs 400 MB), escrito en C, mismo proyecto CNCF. Fluentd sigue siendo válido como aggregator central, pero para DaemonSet en 2026 todo el mundo usa Fluent Bit. Si entregan con Fluentd, la cátedra lo cuenta como mal calibrado (-5 %).
⚠️ NO usen el chart
elastic/elasticsearch“legacy”. Está deprecado desde 2023 a favor del ECK Operator que gestiona el cluster via CRDs. Es el camino oficial Elastic 2026 — y simplifica mucho operaciones como rolling upgrades, cert management, y rotación de TLS.Secret de admin (
elasticuser) gestionado por ECK — el operator lo crea automáticamente. Nunca lo commiteen. Para leerlo:kubectl -n elastic get secret elastic-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -dEl
install.shlo recupera y lo pasa por env var al paso de import del dashboard del Hit #5 — sin escribirlo a archivo.gitleaksen pre-commit y CI: igual que Parte 1. Los certs auto-firmados generados por ECK no se commitean (ECK los regenera en cada install). Si commitean por error la password del usuarioelastico un cert real, el push falla.
Otros requisitos
ADR obligatorio:
docs/adr/0009-stack-de-logging-efk.md(continúa la numeración después del0008-promtail-vs-alloy.mdopcional de Parte 1). No justifica adoptar EFK como stack principal — justifica por qué se evaluó EFK en paralelo a Loki y qué dimensiones se midieron (footprint, latencia de query, ergonomía, licencia). La conclusión queda abierta para que la Parte 4 la consolide. Mismo formato Michael Nygard.📜 Importante — licencia. Elasticsearch / Kibana usan Elastic License v2 (ELv2) desde 2021 (post-fork con OpenSearch). NO es OSS según OSI: prohíbe ofrecer Elasticsearch as-a-service compitiendo con Elastic. Para uso académico / interno como este TP es libre, pero es relevante para el ADR de Parte 4 porque cambia el escenario de adopción en empresa. Si quieren un equivalente 100 % OSS, la alternativa es OpenSearch + OpenSearch Dashboards (fork AWS, Apache 2.0). Mencionar esto en el ADR no es opcional.
NO romper Parte 1. El stack Loki sigue corriendo en
observability. El stack EFK va enelastic. Los dos leen los mismos logs del scraper (ml-scrapernamespace) — eso permite el experimento de comparativa. Si para hacer entrar EFK matan a Loki, perdieron Parte 1 y Parte 4 al mismo tiempo.Resources limitados — calibración honesta. EFK es pesado. Estos son los techos aceptables:
Componente Requests Limits Storage ECK Operator 50m CPU, 128Mi RAM 100m CPU, 256Mi RAM — Elasticsearch (1 nodo) 500m CPU, 1Gi RAM 1000m CPU, 2Gi RAM 10Gi PVC local-pathKibana 200m CPU, 512Mi RAM 500m CPU, 1Gi RAM — Fluent Bit (DaemonSet) 50m CPU, 64Mi RAM 200m CPU, 128Mi RAM — (lee del host) 💡 Comparen con la tabla equivalente de Parte 1: Loki single-binary pedía 256 Mi / 512 Mi de RAM. Elasticsearch pide 4× más y todavía estamos en single-node sin réplicas. Esa diferencia se va a sentir cuando arranquen los dos al mismo tiempo — tomen nota para el ADR.
JVM heap explícito: Elasticsearch corre sobre JVM y por default toma 50 % de la RAM del container. Con 2 GiB de limit, fijen
ES_JAVA_OPTS: "-Xms1g -Xmx1g"(1 GB heap) en el manifest. Si dejan el default y el pod tiene 2 GiB, ES toma 1 GiB de heap igual, pero explícito es mejor que mágico — y el ADR lo va a pedir.
Contenidos del programa relacionados
- Observabilidad: comparativa de stacks de logging — Loki (label-first, Parte 1) vs EFK (full-text inverted index).
- Elasticsearch: motor Lucene-based, inverted index, sharding, ILM (hot/warm/cold/delete).
- Fluent Bit: agente liviano en C (CNCF graduated), pipeline Input → Parser → Filter → Output.
- Kibana: Discover, Dashboard, KQL (Kibana Query Language) vs Lucene query (legacy).
- Kubernetes Operators: CRD-based lifecycle management (ECK como caso de estudio).
- Index lifecycle management: rollover por tamaño/edad/docs, retention as-policy.
- Licenciamiento OSS vs source-available: SSPL, ELv2, Apache 2.0 — implicancia en decisiones de stack.
- Patrones operativos en EFK: shard sizing, refresh intervals, query DSL vs KQL.
Práctica
En la Parte 1 montaron Loki + Promtail + Grafana y vieron que el modelo “label-first” es simple, barato y suficiente para queries operacionales tipo “errores por producto en 24h”. Lo que no vieron es lo que pasa cuando alguien pregunta:
“Encontrame todos los logs donde aparezca la substring
MercadoLibre cookie expireden cualquier campo, incluyendo los stacktraces.”
Con LogQL eso es un grep lineal sobre todos los chunks del stream — funciona, pero es lento (segundos a minutos según volumen). Elasticsearch construye un inverted index sobre el cuerpo del log: la misma query es milisegundos, sin importar cuántos GB tenés. Ese es el caso de uso donde EFK gana. La pregunta del TP es: ¿vale el costo extra de RAM + ops complexity + license restriction?
Para responderla con datos vamos a:
- Levantar EFK al lado de Loki (no en lugar de).
- Mandar los mismos logs del scraper a los dos stacks (Promtail los manda a Loki, Fluent Bit los manda a Elasticsearch).
- Correr queries equivalentes en ambos y comparar latencia, ergonomía y footprint.
- Documentar todo en el ADR
0009para que la Parte 4 cierre la decisión.
Restricciones idénticas a Parte 1 (cluster local, sin cloud, sin servicios pagos) — pero ahora con menos margen de RAM. Si el cluster no aguanta, bajen Loki temporalmente durante el desarrollo y vuelvan a subir los dos para la entrega final. Documentenlo en el README del repo.
Hit #1 — Deploy del ECK Operator + Elasticsearch single-node + Kibana
ECK (Elastic Cloud on Kubernetes) es el operator oficial de Elastic.
Maneja el lifecycle de Elasticsearch / Kibana / Beats / APM Server vía
CRDs. Para esta Parte 2 alcanza con Elasticsearch y
Kibana.
1.1 — Namespace y repo Helm
kubectl create namespace elastic
kubectl create namespace elastic-system # convención: el operator vive aparte
helm repo add elastic https://helm.elastic.co
helm repo update1.2 — Instalar el ECK Operator
efk/helm/eck-operator-values.yaml:
# El operator vigila todo el cluster pero corre en su propio namespace
managedNamespaces: [elastic]
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { cpu: 100m, memory: 256Mi }
# El operator NO es la pieza que necesita HA en este TP
replicaCount: 1helm install eck-operator elastic/eck-operator \
--version 2.16.0 \
--namespace elastic-system \
--values efk/helm/eck-operator-values.yaml
kubectl -n elastic-system rollout status statefulset/elastic-operator --timeout=180s💡 Por qué
StatefulSety noDeployment: el operator usa leader election sobre un PVC para garantizar que sólo una instancia haga reconciliation a la vez. Es transparente — pero si lo ven enkubectl get sts -n elastic-system, no es bug.
1.3 —
Custom Resource Elasticsearch (1 nodo, modo
single-node)
efk/manifests/elasticsearch.yaml:
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: scraper
namespace: elastic
spec:
version: 8.17.3
# Para producción se usan al menos 3 nodos (master quorum). Para el TP, single-node
# es lo único que entra en RAM. ECK acepta un solo nodo si discovery.type=single-node.
nodeSets:
- name: default
count: 1
config:
node.roles: ["master", "data", "ingest"]
# Sin esta línea, ES espera al menos 2 masters y queda en "yellow" para siempre.
node.store.allow_mmap: false
# En cluster local hay un solo nodo => single-shard, sin réplicas.
index.number_of_shards: 1
index.number_of_replicas: 0
podTemplate:
spec:
containers:
- name: elasticsearch
env:
# Heap fijo: 1 GB (50% del limit de 2 GiB)
- name: ES_JAVA_OPTS
value: "-Xms1g -Xmx1g"
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: 1000m, memory: 2Gi }
volumeClaimTemplates:
- metadata:
name: elasticsearch-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-path
resources:
requests:
storage: 10Gikubectl apply -f efk/manifests/elasticsearch.yaml
# El cluster pasa por phases: ApplyingChanges → Ready. Tarda 1-3 min en arrancar la JVM.
kubectl -n elastic get elasticsearch scraper -w
# Esperar a HEALTH=green PHASE=ReadyVerificar via API:
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user \
-o jsonpath='{.data.elastic}' | base64 -d)
kubectl -n elastic port-forward svc/scraper-es-http 9200:9200 &
sleep 2
curl -k -u "elastic:$PASSWORD" https://localhost:9200/_cluster/health | jq
# Esperado: { "status": "green", "number_of_nodes": 1, ... }1.4 — Custom Resource
Kibana
efk/manifests/kibana.yaml:
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
name: scraper
namespace: elastic
spec:
version: 8.17.3
count: 1
elasticsearchRef:
name: scraper
podTemplate:
spec:
containers:
- name: kibana
resources:
requests: { cpu: 200m, memory: 512Mi }
limits: { cpu: 500m, memory: 1Gi }efk/manifests/kibana-nodeport.yaml:
apiVersion: v1
kind: Service
metadata:
name: kibana-nodeport
namespace: elastic
spec:
type: NodePort
selector:
common.k8s.elastic.co/type: kibana
kibana.k8s.elastic.co/name: scraper
ports:
- name: https
port: 5601
targetPort: 5601
nodePort: 30001kubectl apply -f efk/manifests/kibana.yaml
kubectl apply -f efk/manifests/kibana-nodeport.yaml
kubectl -n elastic rollout status deployment/scraper-kb --timeout=180sOutput esperado del Hit #1
$ kubectl -n elastic get elasticsearch,kibana
NAME HEALTH NODES VERSION PHASE AGE
scraper green 1 8.17.3 Ready 4m
NAME HEALTH NODES VERSION AGE
scraper green 1 8.17.3 3m
$ kubectl -n elastic get pods
NAME READY STATUS RESTARTS AGE
scraper-es-default-0 1/1 Running 0 4m
scraper-kb-69b8c4d4-xxxxx 1/1 Running 0 3m
Abrir https://<node-ip>:30001 → aceptar cert
auto-firmado → login con elastic + el password recuperado
del secret. Tienen que ver el welcome page de Kibana
8.17. No hay datos todavía — eso llega en el Hit #2.
Hit #2 — Fluent Bit como DaemonSet, pipeline al scraper
Acá es donde el modelo se diferencia de Promtail (Parte 1). Fluent Bit tiene un pipeline explícito de Input → Parser → Filter → Output que se configura por secciones — más expresivo, más verboso, pero permite cosas que Promtail no (multiline, parsers en C nativo, output a múltiples backends en paralelo).
El objetivo de este Hit: que los mismos JSON logs del scraper
que ya van a Loki vayan también a Elasticsearch, parseados,
enriquecidos con metadata Kubernetes, e indexados en
scraper-logs-*.
2.1 — Helm chart oficial Fluent Bit
helm repo add fluent https://fluent.github.io/helm-charts
helm repo update2.2 — Values con pipeline explícito
efk/helm/fluent-bit-values.yaml:
# Imagen oficial CNCF, no la de "fluent-bit-eck"
image:
repository: fluent/fluent-bit
tag: 3.2.4
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
# Tolera taints del control plane (k3s single-node)
tolerations:
- effect: NoSchedule
operator: Exists
# RBAC para leer pod metadata via API
serviceAccount:
create: true
rbac:
create: true
# Necesita acceder al secret de ES para autenticarse
extraVolumes:
- name: es-certs
secret:
secretName: scraper-es-http-certs-public
extraVolumeMounts:
- name: es-certs
mountPath: /etc/fluent-bit/certs
readOnly: true
env:
- name: ES_PASSWORD
valueFrom:
secretKeyRef:
name: scraper-es-elastic-user
key: elastic
config:
service: |
[SERVICE]
Daemon Off
Flush 5
Log_Level info
Parsers_File /fluent-bit/etc/parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
inputs: |
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*ml-scraper*.log
Parser cri
DB /var/log/flb_kube.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
filters: |
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Parser On
K8S-Logging.Exclude On
Annotations Off
Labels On
# Sólo nos quedamos con pods cuyo label app=scraper (igual que Hit #2 de Parte 1)
[FILTER]
Name grep
Match kube.*
Regex $kubernetes['labels']['app'] ^scraper$
# Parsea el JSON-line emitido por logging_setup.py (ya estructurado del Hit #3 de Parte 1)
[FILTER]
Name parser
Match kube.*
Key_Name log
Parser json_scraper
Reserve_Data On
Preserve_Key Off
outputs: |
[OUTPUT]
Name es
Match kube.*
Host scraper-es-http.elastic.svc.cluster.local
Port 9200
HTTP_User elastic
HTTP_Passwd ${ES_PASSWORD}
tls On
tls.verify Off
Suppress_Type_Name On
Logstash_Format On
Logstash_Prefix scraper-logs
Logstash_DateFormat %Y.%m.%d
Time_Key @timestamp
Replace_Dots On
Retry_Limit 5
customParsers: |
[PARSER]
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<log>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
[PARSER]
Name json_scraper
Format json
Time_Key timestamp
Time_Format %Y-%m-%dT%H:%M:%S%z
Time_Keep Onhelm install fluent-bit fluent/fluent-bit \
--version 0.48.5 \
--namespace elastic \
--values efk/helm/fluent-bit-values.yaml
kubectl -n elastic rollout status ds/fluent-bit --timeout=120s💡 Por qué
Logstash_Formatsi no usamos Logstash: el outputesde Fluent Bit usa la convención de índices con sufijo de fecha (scraper-logs-2026.05.10) — esa convención la heredó de Logstash y se la sigue llamando “Logstash format” aunque acá no haya Logstash en ningún lado. Tener un índice por día es lo que después permite el rollover de ILM (Hit #3) sin pelear con scripts.
2.3 — Disparar tráfico para validar
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly scraper-efk-test-1
kubectl -n ml-scraper wait --for=condition=complete job/scraper-efk-test-1 --timeout=600sOutput esperado del Hit #2
# Verificar que el índice se creó y tiene docs
kubectl -n elastic port-forward svc/scraper-es-http 9200:9200 &
sleep 2
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d)
curl -sk -u "elastic:$PASSWORD" "https://localhost:9200/_cat/indices?v" | grep scraper
# Esperado: yellow open scraper-logs-2026.05.10 ... N docsY en Kibana → Stack Management → Index
Management → tiene que aparecer
scraper-logs-YYYY.MM.DD. En Discover
(después de crear el data view del Hit #3) van a aparecer los logs con
todos los campos JSON parseados: level,
producto, logger, message,
kubernetes.namespace, kubernetes.pod_name,
etc.
Capturen un screenshot de Kibana → Discover
mostrando los campos extraídos y commitéenlo en
efk/screenshots/hit2-fluentbit-discover.png.
Hit #3 — Index pattern + ILM (rollover, retention 7 días)
Sin política de retención, Elasticsearch acumula índices
indefinidamente y termina llenando el disco. ILM (Index
Lifecycle Management) es el equivalente Elastic del
retention_period: 168h que usaron en Loki Parte 1 — pero
más expresivo: define fases (hot / warm / cold / frozen
/ delete) con transiciones por edad, tamaño o número de docs.
Para esta Parte 2 alcanza con un ciclo simple: hot 1 día → warm 6 días → delete.
3.1 — Crear data view en Kibana
Stack Management → Data Views → Create data view:
- Nombre:
scraper-logs - Index pattern:
scraper-logs-* - Timestamp field:
@timestamp
Guardar. Esto permite que Discover, Dashboards, y Lens “vean” los índices.
3.2 — ILM policy
efk/manifests/ilm-policy.json:
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_age": "1d",
"max_primary_shard_size": "1gb"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "1d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"delete": {
"min_age": "7d",
"actions": {
"delete": { "delete_searchable_snapshot": true }
}
}
}
}
}Aplicar via API:
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d)
curl -sk -u "elastic:$PASSWORD" \
-X PUT "https://localhost:9200/_ilm/policy/scraper-logs" \
-H "Content-Type: application/json" \
-d @efk/manifests/ilm-policy.json
# Esperado: {"acknowledged": true}3.3 — Index
template que asocia ILM a scraper-logs-*
curl -sk -u "elastic:$PASSWORD" \
-X PUT "https://localhost:9200/_index_template/scraper-logs-template" \
-H "Content-Type: application/json" \
-d '{
"index_patterns": ["scraper-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"index.lifecycle.name": "scraper-logs",
"index.lifecycle.rollover_alias": "scraper-logs"
}
}
}'⚠️ Single-node =
number_of_replicas: 0o el cluster queda enyellowpermanente. En producción real (3+ nodos) usennumber_of_replicas: 1mínimo.
Output esperado del Hit #3
En Kibana → Stack Management → Index
Lifecycle Policies → aparece la policy
scraper-logs con las 3 fases. Y en Index
Management → cada índice scraper-logs-YYYY.MM.DD
tiene la columna Lifecycle policy = scraper-logs y
muestra la fase actual.
Para ver que el rollover efectivamente funciona durante el desarrollo (sin esperar 24h), pueden forzarlo:
curl -sk -u "elastic:$PASSWORD" \
-X POST "https://localhost:9200/scraper-logs/_rollover" \
-H "Content-Type: application/json" \
-d '{"conditions":{"max_age":"0ms"}}'Capturen el screenshot de la policy en
efk/screenshots/hit3-ilm-policy.png.
Hit #4 — Cookbook KQL: 6+ queries útiles
Documenten en efk/queries/kql-cookbook.md al
menos 6 queries en KQL (Kibana Query Language, NO Lucene query
— language: lucene está deprecado desde 8.0). Cada query
lleva: pregunta de negocio · query · screenshot del resultado ·
comentario.
💡 KQL vs Lucene query: Lucene es el lenguaje viejo (
field:value AND field:value). KQL es el nuevo (field: value and field: value), case-insensitive en operadores, soporta wildcards más predecibles, y es el default de Kibana 8+. Para esta entrega usen KQL exclusivamente. En el cookbook dejen una nota mencionando el equivalente Lucene de cada query — la cátedra valora que sepan que existe la diferencia.
Mínimo obligatorio (6 queries que la cátedra valida — pueden agregar más):
| # | Pregunta | Query KQL |
|---|---|---|
| Q1 | Errores por producto en las últimas 24h | level: "ERROR" and producto: * (filtro de tiempo en el
time picker) |
| Q2 | Top selectores faltantes (filtros que el scraper no encontró) | message: "Filtro * no disponible" and producto: * |
| Q3 | Distribución de duración del Job (campo job_duration_ms
del scraper) |
event: "scrape_completado" and job_duration_ms >= 0
(visualizar como histograma) |
| Q4 | Logs con timeout de Selenium en cualquier producto | message: *timeout* and (logger: "selenium*" or logger: "extractors") |
| Q5 | Eventos del CronJob específico (correlación por
job_name) |
kubernetes.labels.job_name: "scraper-test-1" |
| Q6 | Errores que NO sean del módulo de Postgres (excluir false positives conocidos) | level: "ERROR" and not logger: "psycopg*" |
Para cada una, documenten en el cookbook:
- La pregunta de negocio (1 línea).
- La query KQL exacta (sin
_sourcefilters — KQL no maneja eso, eso es Query DSL). - El equivalente Lucene (1 línea, para mostrar que entendieron la diferencia).
- Un screenshot del resultado en Discover, con time range relevante visible.
- Por qué la query está escrita así — sobre todo:
cuándo conviene
*(wildcard expensive) vs un campokeyword(fast).
📚 Documentación KQL canónica: https://www.elastic.co/guide/en/kibana/current/kuery-query.html. Cortita y al hueso. La sección “Query and filter context” es la que se les va a hacer útil cuando pasen una query de Discover a un panel de dashboard.
Equivalente entre KQL y LogQL
Comparen mentalmente con las queries del cookbook de Parte 1.
Q1 acá vs Q1 de Parte 1: ambas responden la misma
pregunta, pero el modelo es distinto — Loki cuenta sobre streams
etiquetados, Elasticsearch agrega sobre documents indexados. El ADR
0009 tiene que tocar este punto.
Hit #5 — Dashboard Kibana provisionado as-code (NDJSON + import API)
A diferencia de Grafana (donde el dashboard JSON va a un ConfigMap
montado), Kibana usa import via REST API: los saved
objects se exportan como NDJSON (newline-delimited
JSON, un objeto por línea) y se importan con
POST /api/saved_objects/_import.
5.1 — Construir el dashboard en la UI
Construyan un dashboard único en Kibana → Dashboards → Create dashboard con al menos 6 paneles (más que los 5 del dashboard equivalente de Parte 1 — Kibana es más rico para visualizaciones agregadas):
- Metric — Total de eventos hoy
(
@timestamplast 24h). - Metric — % de eventos
level: "ERROR"vs total. - Bar chart vertical — Top 5 productos con más errores (Q1 del cookbook).
- Pie chart — Distribución por
level(INFO / WARNING / ERROR). - Line chart — Eventos por minuto last 6h, breakdown
por
level. - Table — Última corrida exitosa por producto (Q de “max @timestamp by producto where event=scrape_completado”).
Bonus aceptado (no obligatorio):
- Heatmap — Distribución hora-del-día × producto de errores last 7d (revela patrones tipo “ML rate-limita en horario laboral”).
- Lens (auto) — campo
job_duration_mscon percentiles 50 / 95 / 99.
5.2 — Exportar a NDJSON
Stack Management → Saved Objects → seleccionar el
dashboard + sus visualizaciones + el data view → Export
→ guardar como efk/dashboards/scraper-overview.ndjson.
⚠️ Marcar “Include related objects” o el NDJSON va a quedar incompleto y el import en otro cluster va a fallar con “Missing references”.
5.3 — Import via API en
install.sh
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d)
# Port-forward a Kibana
kubectl -n elastic port-forward svc/scraper-kb-http 5601:5601 &
PF_PID=$!
sleep 5
curl -sk -u "elastic:$PASSWORD" \
-X POST "https://localhost:5601/api/saved_objects/_import?overwrite=true" \
-H "kbn-xsrf: true" \
-F file=@efk/dashboards/scraper-overview.ndjson | jq '.success'
# Esperado: true
kill $PF_PID💡 El header
kbn-xsrf: truees obligatorio. Kibana exige CSRF token en todos los POST/PUT/DELETE — sin ese header el request falla con 400 incluso con auth válida. Es el primer pitfall que encuentran todos en su primer intento.
Output esperado del Hit #5
Después de re-aplicar y re-disparar el
scraper-efk-test-1:
- En Kibana → Dashboards → aparece Scraper Overview.
- Los 6 paneles muestran datos reales (no “No results found”).
- El dashboard se renderea en menos de 5 segundos contra los datos del último día.
Capturen el screenshot final en
efk/screenshots/hit5-dashboard.png — esto es lo
que la cátedra mira primero.
Hit #6 — Alertas via Kibana Alerting (opcional, bonus +5 %)
Configuren una rule en Kibana Alerting que dispare cuando se cumpla:
Más de 5 eventos
level: "ERROR"del scraper en cualquier ventana de 1 hora.
Notificación al mismo Discord webhook que usaron en Parte 1 — eso permite que la alerta de Loki y la de EFK aparezcan en el mismo canal y la cátedra pueda verificar las dos.
Estructura mínima
Connector Discord en Stack Management → Alerts and Insights → Connectors → Create connector → tipo Webhook (Kibana no tiene un connector “Discord” nativo, pero Discord acepta webhooks genéricos POST con JSON).
🔒 Igual que Parte 1, el webhook URL es secret. No commitearlo. Pasen
DISCORD_WEBHOOK_URLal install.sh por env var y créenlo via API:curl -sk -u "elastic:$PASSWORD" \ -X POST "https://localhost:5601/api/actions/connector" \ -H "kbn-xsrf: true" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"discord-sip2026\", \"connector_type_id\": \".webhook\", \"config\": { \"url\": \"${DISCORD_WEBHOOK_URL}\", \"method\": \"post\", \"headers\": { \"Content-Type\": \"application/json\" } } }"Rule type: Elasticsearch query (KQL). Ejemplo:
Index:
scraper-logs-*Time field:
@timestampQuery:
level: "ERROR"Threshold:
IS ABOVE 5Time window:
1hCheck every:
5mAction: el connector Discord con body:
{ "content": "ALERTA SIP 2026 (EFK): {{context.hits}} errores del scraper en 1h. Producto top: {{context.value}}" }
Test: simulen el threshold bajándolo a
IS ABOVE 0durante la corrida de evaluación, o disparen 6 jobs forzados con bug intencional para que generen ERRORs reales.
Documenten en efk/README.md cómo se setea
DISCORD_WEBHOOK_URL y commiteen el screenshot del mensaje
de alerta en
efk/screenshots/hit6-discord-alert-efk.png.
Cómo entregar
- Push final al repo público (mismo repo del TP 1 + Parte 1) antes del 05/05/2026 23:59 ART.
- README raíz actualizado con sección nueva “TP 2 ·
Parte 2 — EFK”:
- Cómo ejecutar
efk/install.shdesde cero. - Variables de entorno requeridas (
DISCORD_WEBHOOK_URLopcional para Hit #6 — la password deelasticla genera ECK sola). - Link al
efk/README.mdcon detalles.
- Cómo ejecutar
- Carpeta
efk/completa según estructura obligatoria. docs/adr/0009-stack-de-logging-efk.mdcomparando ergonomía / footprint / licencia vs Loki.- Carpeta
efk/screenshots/con mínimo:hit2-fluentbit-discover.png— Discover mostrando los logs con campos JSON parseados.hit3-ilm-policy.png— la policyscraper-logscon sus 3 fases.hit5-dashboard.png— dashboard Scraper Overview renderizado con datos reales.- (bonus)
hit6-discord-alert-efk.png— alerta de EFK en Discord.
- Video corto (3-5 min) mostrando:
install.shcorriendo de cero, Kibana abriéndose en:30001, demo de las 6 queries del Hit #4 en Discover, dashboard del Hit #5 con datos. - Mensaje en el canal Discord de la materia con link al repo + video.
📡 Canal Discord: https://discord.com/channels/1482135908508500148/1482135909456679139 Antes de pedir ayuda, revisá Common pitfalls.
Auto-verificación previa a la entrega
Si algo de esta lista falla, no entregues. 8 puntos seguros que se pierden con 5 minutos de checklist.
1)
install.sh corre limpio en cluster vacío
kubectl delete namespace elastic --wait=true
kubectl delete namespace elastic-system --wait=true
cd efk && ./install.sh
# Tiene que terminar con exit 0 y los 7 ✓ del output esperado del Hit #12) Elasticsearch
está green y Kibana available
kubectl -n elastic get elasticsearch,kibana
# elasticsearch HEALTH=green PHASE=Ready
# kibana HEALTH=green3) Fluent Bit DaemonSet está Running en todos los nodos
kubectl -n elastic get ds fluent-bit
# DESIRED == READY (en cluster single-node ambos = 1)
kubectl -n elastic logs ds/fluent-bit | tail -20
# Tiene que mostrar líneas tipo "[output:es:es.0] HTTP status=200/201"4) Elasticsearch responde a queries via HTTP
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d)
kubectl -n elastic port-forward svc/scraper-es-http 9200:9200 &
sleep 2
curl -sk -u "elastic:$PASSWORD" "https://localhost:9200/_cat/indices?v" | grep scraper-logs
# Esperado: al menos 1 línea con "open" e índice yellow/green con docs > 05) ILM policy
scraper-logs aplicada
curl -sk -u "elastic:$PASSWORD" "https://localhost:9200/_ilm/policy/scraper-logs" | jq '.scraper-logs.policy.phases | keys'
# Esperado: ["delete","hot","warm"]6) Las 6 queries KQL del Hit #4 corren sin error
Abran Kibana → Discover → data view scraper-logs →
peguen cada query del cookbook. Ninguna debe dar error de
sintaxis. Pueden devolver 0 resultados si no hay datos en ese
rango, pero no rojo.
7) Dashboard provisionado aparece en la UI
# Después de install.sh:
# Abrir Kibana → Dashboards → buscar "Scraper Overview" → tiene que estar
# Abrirlo → los 6 paneles tienen que renderear (datos o "No results", pero no error)8)
gitleaks no detecta el password de Elastic ni el webhook
Discord
gitleaks detect --no-git --verbose
# Esperado: 0 leaks. Ni la password de "elastic" ni el DISCORD_WEBHOOK_URL en el repo.Common pitfalls
vm.max_map_count
muy bajo en el host
Elasticsearch necesita vm.max_map_count >= 262144. En
k3d / Docker Desktop el default es 65536 y ES se cae al arrancar con
error
max virtual memory areas vm.max_map_count [65530] is too low.
Mitigación:
sudo sysctl -w vm.max_map_count=262144
# Persistir en /etc/sysctl.d/99-elastic.confEn k3d puro hay que ejecutarlo en el host del Docker, no dentro del contenedor.
Cluster yellow
permanente
Causa #1: number_of_replicas > 0 con un solo nodo.
Las réplicas no pueden asignarse y el cluster queda yellow.
Solución: en single-node siempre replicas: 0.
Causa #2: el .kibana_* internal index tiene réplicas hardcoded. Inofensivo en single-node, pero molesta en logs. Documentado en el ADR como “expected en dev cluster”.
Fluent Bit no manda nada a ES
Mirar
kubectl -n elastic logs ds/fluent-bit | grep -i error.
Causas frecuentes:
- TLS verify falla — el cert auto-firmado de ES no es
trusted. Mitigación:
tls.verify Offen el outputes(ok para single-cluster local; NO hacer esto en prod). - Auth falla — verificar que el secret
scraper-es-elastic-userestá montado y que${ES_PASSWORD}se sustituye en runtime. - Pipeline backpressure — si ES está OOMeando, Fluent
Bit empieza a tirar
[ warn] [engine] failed to flush chunk. La solución no es subirMem_Buf_Limit— es bajar el ingest o subir el heap de ES.
kbn-xsrf: true
falta en el import
Si el curl al endpoint _import devuelve 400
con "Request must contain a kbn-xsrf header", agreguen el
header. Es el footgun #1 de la API de Kibana — todo POST / PUT / DELETE
lo necesita.
El NDJSON exportado no incluye el data view
Cuando exportan el dashboard sin marcar “Include related objects”, el
archivo no incluye el data view scraper-logs y al importar
en un cluster limpio explota con “Missing references: index-pattern:
scraper-logs”. Siempre exportar con la opción
tildada.
Memoria — el cluster local muere
Síntoma: kubectl get pods -A muestra Loki y/o ES en
OOMKilled. Causa: 6 GB no alcanzan para los dos stacks +
Postgres + scraper. Mitigación durante el desarrollo: mantener uno de
los dos stacks en replicas: 0 y subirlos los dos sólo para
la corrida final de evaluación. Documentarlo en
efk/README.md (“comportamiento esperado en cluster < 8
GB”).
Confundir KQL con Lucene query language
Si pegan level:ERROR AND producto:iphone (sin espacios
después de :, AND mayúscula), funciona en
Lucene pero NO en KQL — KQL espera
level: "ERROR" and producto: "iphone". La barra de Discover
por default usa KQL desde 8.x; el toggle a Lucene está en el ícono de la
lupita.
Criterios de evaluación
Requisitos bloqueantes
- TP 2 · Parte 1 entregada y aprobada (≥ 60/100). Los dos stacks van a coexistir, sin Parte 1 no hay comparativa.
efk/install.shfunciona en cluster limpio (verificado en auto-verificación #1).- Versiones pinneadas a 8.17.x (ES/Kibana), 2.16.x
(ECK), 3.2.x (Fluent Bit). No
latest. - No usar Fluentd. Si entregaron con Fluentd, -5 % automático.
- No usar el chart deprecado
elastic/elasticsearch— sólo ECK Operator. - Sin secrets en el repo
(
gitleaks detectda 0 — verificado en auto-verificación #8). - Auto-verificación completa ejecutada antes del push final.
Tabla de puntaje (100 %)
| Criterio | Peso |
|---|---|
| Hit #1 — ECK Operator + Elasticsearch (green) + Kibana corriendo | 20 % |
| Hit #2 — Fluent Bit DaemonSet + pipeline al scraper + parser JSON funcionando | 20 % |
Hit #3 — ILM policy scraper-logs (3
fases) + index template asociado |
15 % |
Hit #4 — kql-cookbook.md con las 6
queries documentadas (pregunta + KQL + equivalente Lucene + por
qué) |
15 % |
Hit #5 — dashboard
scraper-overview.ndjson importado as-code y mostrando datos
reales |
20 % |
ADR 0009-stack-de-logging-efk.md
comparando contra Loki en footprint / latency / lic. |
10 % |
| Bonus Hit #6 — alerta a Discord funcionando + screenshot | +5 % |
Material de apoyo
Tabla comparativa Loki (Parte 1) vs EFK (Parte 2) — números honestos
Esta tabla es el insumo principal del ADR
0009 y de la decisión final de la Parte 4. Mídanla
en su propio cluster — los números abajo son referencia ballpark con
datos del scraper.
| Dimensión | Loki + Promtail + Grafana | EFK = Elasticsearch + Fluent Bit + Kibana |
|---|---|---|
| RAM resident del backend | ~250-400 Mi (Loki single-binary) | ~1.5-2.0 Gi (ES single-node con heap 1G) |
| RAM resident del agente | ~50-100 Mi (Promtail) | ~40-80 Mi (Fluent Bit, escrito en C — más liviano) |
| RAM resident del visualizador | ~150-250 Mi (Grafana) | ~600-900 Mi (Kibana, Node.js + Chrome PDF) |
| Total RAM stack | ~0.5-0.8 Gi | ~2.5-3.0 Gi (5× más pesado) |
| Disco para 7 días retention | ~500 Mi - 2 Gi (chunks comprimidos) | ~3-8 Gi (índice invertido + .doc files) |
| Latency query “errores 24h” | ~100-300 ms (filtros sobre labels) | ~20-80 ms (inverted index hit) |
| Latency query full-text “encontrar substring” | ~5-30 segundos (grep lineal sobre chunks) | ~50-200 ms (inverted index) |
| Cardinality tolerance | Baja — explota con > 100 valores únicos por label | Alta — Lucene maneja millones de terms por field |
| Setup complexity | 3 charts Helm + 1 ConfigMap dashboard | ECK Operator + 2 CRDs + 1 chart Fluent Bit + import API |
| Query language | LogQL (label-first, PromQL-like) | KQL (campo-valor, intuitivo) + Query DSL JSON (avanzado) |
| Visualización | Grafana (dashboards as-code, alerting maduro) | Kibana (Lens auto, machine learning add-on, más visualizaciones) |
| Licencia | Apache 2.0 (verdaderamente OSS) | Elastic License v2 (source-available, NO OSS, prohíbe SaaS competing) |
| Alternativa OSS | — (ya es Apache 2.0) | OpenSearch + OpenSearch Dashboards (fork AWS, Apache 2.0) |
| Costo cluster local | $0 — entra cómodo en 6 GB | $0 — pero pide 8 GB mínimo |
| Costo cloud (estimado, 100 GB/día) | ~$30/mes (Grafana Cloud Logs) | ~$200/mes (Elastic Cloud) — 6-7× más caro |
| Curva de aprendizaje | Empinada al principio (LogQL es nuevo), después rápida | Empinada al principio (Query DSL es complejo), KQL es amigable |
| Caso donde gana | Logs operacionales, label-driven, bajo volumen | Full-text search, alta cardinalidad, búsqueda exploratoria |
Lectura honesta de la tabla:
- Si lo único que querés es “ver logs del scraper agrupados por producto”, Loki gana en costo / simplicidad por al menos 5×.
- Si tu caso de uso real incluye “encontrame el log con esta substring de 100 chars” — auditoría, debugging post-mortem, security investigations — EFK es insustituible. LogQL no compite ahí.
- Si vas a ir a SaaS / cloud, el costo de EFK / Elastic Cloud es 6-7× el de Grafana Cloud Logs para el mismo volumen. Eso pesa mucho en empresa.
- Si el equipo se preocupa por OSS de verdad (no source-available), Elasticsearch sale del menú. La opción es OpenSearch — el fork AWS post-cambio de licencia 2021. Compatible al 99 % con dashboards Kibana actuales hasta Kibana 7.10 (después divergen).
Esqueleto de
install.sh
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "→ Namespaces"
kubectl create namespace elastic --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace elastic-system --dry-run=client -o yaml | kubectl apply -f -
echo "→ Helm repos"
helm repo add elastic https://helm.elastic.co >/dev/null 2>&1 || true
helm repo add fluent https://fluent.github.io/helm-charts >/dev/null 2>&1 || true
helm repo update >/dev/null
echo "→ ECK Operator"
helm upgrade --install eck-operator elastic/eck-operator \
--version 2.16.0 \
--namespace elastic-system \
--values "$DIR/helm/eck-operator-values.yaml" \
--wait --timeout 5m
echo "→ Elasticsearch + Kibana via CRDs"
kubectl apply -f "$DIR/manifests/elasticsearch.yaml"
kubectl apply -f "$DIR/manifests/kibana.yaml"
kubectl apply -f "$DIR/manifests/kibana-nodeport.yaml"
echo "→ Esperando Elasticsearch ready (puede tardar 2-3 min)"
kubectl -n elastic wait --for=jsonpath='{.status.health}'=green elasticsearch/scraper --timeout=300s
kubectl -n elastic wait --for=jsonpath='{.status.health}'=green kibana/scraper --timeout=300s
echo "→ Fluent Bit"
helm upgrade --install fluent-bit fluent/fluent-bit \
--version 0.48.5 \
--namespace elastic \
--values "$DIR/helm/fluent-bit-values.yaml" \
--wait --timeout 3m
echo "→ ILM policy + index template"
PASSWORD=$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d)
kubectl -n elastic port-forward svc/scraper-es-http 9200:9200 >/dev/null 2>&1 &
PF_ES=$!
trap "kill $PF_ES 2>/dev/null || true" EXIT
sleep 3
curl -sk -u "elastic:$PASSWORD" -X PUT "https://localhost:9200/_ilm/policy/scraper-logs" \
-H "Content-Type: application/json" -d @"$DIR/manifests/ilm-policy.json" >/dev/null
curl -sk -u "elastic:$PASSWORD" -X PUT "https://localhost:9200/_index_template/scraper-logs-template" \
-H "Content-Type: application/json" -d '{
"index_patterns": ["scraper-logs-*"],
"template": {
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0,
"index.lifecycle.name": "scraper-logs",
"index.lifecycle.rollover_alias": "scraper-logs"
}
}
}' >/dev/null
echo "→ Import dashboard NDJSON"
kubectl -n elastic port-forward svc/scraper-kb-http 5601:5601 >/dev/null 2>&1 &
PF_KB=$!
trap "kill $PF_ES $PF_KB 2>/dev/null || true" EXIT
sleep 5
curl -sk -u "elastic:$PASSWORD" \
-X POST "https://localhost:5601/api/saved_objects/_import?overwrite=true" \
-H "kbn-xsrf: true" \
-F file=@"$DIR/dashboards/scraper-overview.ndjson" >/dev/null
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo ""
echo "✓ ECK Operator running"
echo "✓ Elasticsearch green"
echo "✓ Kibana available"
echo "✓ Fluent Bit DaemonSet ready"
echo "✓ ILM policy 'scraper-logs' aplicada"
echo "✓ Index template asociado"
echo "✓ Dashboard 'Scraper Overview' importado"
echo "→ Abrir https://${NODE_IP}:30001 (elastic / \$(kubectl -n elastic get secret scraper-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d))"chmod +x install.sh antes de commitear.
Esqueleto
de fluent-bit-config.yaml (referencia rápida)
Si ven errores en el pipeline de Fluent Bit, este es el esqueleto mínimo que funciona — cualquier error suyo está en una de estas 4 secciones:
[SERVICE]
Daemon Off
Flush 5
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser cri
DB /var/log/flb_kube.db
[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Merge_Log On
K8S-Logging.Parser On
[FILTER]
Name grep
Match kube.*
Regex $kubernetes['labels']['app'] ^scraper$
[OUTPUT]
Name es
Match kube.*
Host scraper-es-http.elastic.svc.cluster.local
Port 9200
HTTP_User elastic
HTTP_Passwd ${ES_PASSWORD}
tls On
tls.verify Off
Logstash_Format On
Logstash_Prefix scraper-logs
Replace_Dots OnEsqueleto de query KQL (referencia rápida)
# Sintaxis básica — campo: valor
level: "ERROR"
# AND / OR / NOT (lower-case en KQL)
level: "ERROR" and producto: "iphone"
level: "ERROR" or level: "WARNING"
level: "ERROR" and not logger: "psycopg*"
# Wildcard (caro en campos text, barato en keyword)
message: *timeout*
producto: *
# Rangos
job_duration_ms >= 5000
@timestamp >= "2026-05-10T00:00:00Z" and @timestamp < "2026-05-11T00:00:00Z"
# Existence
producto: *
# Nested fields (kubernetes metadata)
kubernetes.namespace: "ml-scraper" and kubernetes.labels.app: "scraper"
# Múltiples valores (in)
level: ("ERROR" or "WARNING") and producto: ("iphone" or "samsung")
Esqueleto de ADR
0009-stack-de-logging-efk.md
Mismo formato Michael Nygard de los ADRs anteriores. Esqueleto mínimo:
# 0009 — Evaluación de EFK como segundo stack de logging
- Date: 2026-05-20
- Status: Proposed (cierre formal en ADR 0010 de Parte 4)
- Deciders: <equipo>
## Contexto
En Parte 1 adoptamos Loki + Promtail + Grafana (ADR 0007). En Parte 2 desplegamos EFK
en paralelo con dos objetivos: (a) obtener datos comparativos reales sobre el mismo
workload (scraper) y (b) entender los trade-offs antes de cerrar la decisión final
en Parte 4 con OTel.
Restricciones medidas:
- Cluster k3s single-node, ~8 GB RAM totales (con Loki y EFK simultáneos: ~3.5 GB).
- Mismos logs JSON del scraper (Hit #3 de Parte 1) van a los dos stacks.
- Retención 7 días en ambos.
- Licenciamiento: Loki es Apache 2.0; Elasticsearch es Elastic License v2 (source-available,
NO OSS según OSI). En contexto académico no afecta; en empresa puede ser bloqueante.
## Decisión
NO se decide reemplazar Loki por EFK ni viceversa en esta Parte 2. La decisión se difiere
al ADR 0010 (Parte 4) cuando se haya evaluado también OTel.
Lo que sí se decide acá:
- Mantener los dos stacks corriendo en paralelo durante el TP.
- Documentar las dimensiones de comparación (ver tabla en TP 2 · Parte 2).
- Marcar EFK como "candidato fuerte" cuando el caso de uso requiera full-text search
pesado, y "candidato descartado" cuando el footprint o licencia importen.
## Consecuencias
- Se gana visibilidad real sobre los trade-offs (no opinión, datos del propio cluster).
- Se gana experiencia con ECK Operator + ILM + KQL — útil aunque no se adopte EFK como
principal.
- Se pierde RAM (~3.5 GB) durante el desarrollo. Mitigado bajando uno de los dos stacks
durante coding y subiendo ambos para evaluación.
- Riesgo: introducir dependencia de Elastic License v2 en el repo. Mitigado: sólo se
usan imágenes oficiales, no se redistribuye Elasticsearch ni se ofrece como servicio.
## Métricas medidas (en NUESTRO cluster)
- RAM Loki stack: `<medir>` Mi
- RAM EFK stack: `<medir>` Mi (ratio: `<calcular>`×)
- Latency Q1 (errores por producto 24h):
- Loki: `<medir>` ms
- EFK: `<medir>` ms
- Latency full-text "encontrar substring de 50 chars" en 7 días de logs:
- Loki: `<medir>` seg
- EFK: `<medir>` ms
## Referencias
- Tabla comparativa de la cátedra: TP 2 · Parte 2 / Material de apoyo
- Elastic License v2: https://www.elastic.co/licensing/elastic-license
- OpenSearch (alternativa OSS): https://opensearch.org/Plantilla del ADR comparativo final (Parte 4)
La Parte 4 va a consolidar los ADR 0007 (Loki),
0009 (EFK) y el que produzca la Parte 3 (OTel) en un
ADR 0012-stack-de-observabilidad-final.md.
La cátedra ya tiene un esqueleto preparado — no se entrega en esta Parte
2, pero escriban el 0009 sabiendo que va a ser
revisitado. Si lo escriben honesto y con datos, la Parte 4 va a
ser corta. Si lo escriben con “Loki es mejor porque sí”, la Parte 4 los
va a obligar a re-medir.
Referencias y Bibliografía
Solo lo directamente vinculado a esta Parte 2. Los libros generales
viven en TP 0; las referencias de Parte 1 viven en
TP3-1.md. No se repiten.
Hit #1 — ECK Operator + Elasticsearch + Kibana
- Elastic Cloud on Kubernetes — Quickstart — el camino oficial Elastic 2026 para K8s. Cubre la instalación del operator y los primeros CRDs. https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-quickstart.html
- ECK Operator — Release notes — chequear que la versión que usen (2.16.x) está soportada con Elasticsearch 8.17.x. https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-release-notes.html
- Elasticsearch — Single-node configuration —
discovery.type: single-nodey por qué solo aplica para dev/test, nunca producción. https://www.elastic.co/guide/en/elasticsearch/reference/current/discovery-settings.html - Kibana — Configure Kibana on Kubernetes (ECK) — el
CRD
Kibana,elasticsearchRef, y cómo se monta el secret de auto-discovery. https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-kibana.html
Hit #2 — Fluent Bit + pipeline Kubernetes
- Fluent Bit — Kubernetes filter — la sección clave
del pipeline: cómo enriquecer cada record con
kubernetes.labels.*,kubernetes.namespace, etc. https://docs.fluentbit.io/manual/pipeline/filters/kubernetes - Fluent Bit — Tail input plugin — opciones del
[INPUT]que recolecta de/var/log/containers/:Path,Parser,DB,Refresh_Interval. https://docs.fluentbit.io/manual/pipeline/inputs/tail - Fluent Bit — Elasticsearch output —
Logstash_Format,tls.verify,Replace_Dots, retry semantics. https://docs.fluentbit.io/manual/pipeline/outputs/elasticsearch - Fluent Bit — Helm chart values — referencia
completa de
values.yamlpara el chart oficial. https://github.com/fluent/helm-charts/tree/main/charts/fluent-bit - CNCF — Fluent Bit vs Fluentd — la comparativa oficial donde se argumenta por qué Fluent Bit es la elección moderna para DaemonSet. https://docs.fluentbit.io/manual/about/fluentd-and-fluent-bit
Hit #3 — Index Lifecycle Management
- Elasticsearch — ILM overview — el modelo hot/warm/cold/frozen/delete y cuándo aplica cada uno. https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html
- Elasticsearch — Rollover action —
max_age,max_primary_shard_size,max_docs— la unidad de la fase hot. https://www.elastic.co/guide/en/elasticsearch/reference/current/ilm-rollover.html - Elasticsearch — Index templates — cómo asociar ILM policies a un patrón de índices via template. https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html
Hit #4 — KQL
- Kibana — KQL syntax — sintaxis de KQL:
field: value,and/or/not, wildcards, ranges. https://www.elastic.co/guide/en/kibana/current/kuery-query.html - Kibana — Query DSL vs KQL — cuándo conviene cada uno (KQL para Discover / dashboards; Query DSL para visualizations complejas con aggregations custom). https://www.elastic.co/guide/en/kibana/current/discover-search-for-relevance.html
- Elasticsearch — Query DSL (referencia, no obligatorio para esta entrega) — el JSON query language que está debajo de KQL. https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
Hit #5 — Saved Objects API + dashboards
- Kibana — Saved Objects API —
_export,_import,_resolve_import_errors. La que usa elinstall.sh. https://www.elastic.co/guide/en/kibana/current/saved-objects-api.html - Kibana — Import saved objects via API — el ejemplo
exacto del
curlconkbn-xsrfymultipart/form-data. https://www.elastic.co/guide/en/kibana/current/saved-objects-api-import.html - Kibana — Dashboard NDJSON format — la estructura de los saved objects exportados (data view, lens, dashboard). https://www.elastic.co/guide/en/kibana/current/dashboard.html
Hit #6 — Kibana Alerting (bonus)
- Kibana Alerting — Rule types — los tipos de rule disponibles (Elasticsearch query, Threshold, Anomaly detection). https://www.elastic.co/guide/en/kibana/current/alerting-getting-started.html
- Kibana Alerting — Webhook connector — el connector genérico que usamos para mandar a Discord (Discord acepta el body JSON estándar). https://www.elastic.co/guide/en/kibana/current/webhook-action-type.html
Licencia y alternativas OSS
- Elastic License v2 — el texto oficial de la licencia source-available que aplica a Elasticsearch / Kibana desde 2021. https://www.elastic.co/licensing/elastic-license
- OpenSearch — Project home — el fork de AWS post-cambio de licencia, Apache 2.0. La alternativa real si “OSS” es requisito hard. https://opensearch.org/
- OSI — License Compatibility / SSPL discussion — por qué Elastic License v2 (y SSPL antes) no son OSS según OSI. Lectura para el ADR. https://opensource.org/licenses
Comparativa Loki vs Elasticsearch
- Grafana — “Like Prometheus, but for logs” (referenciado en Parte 1) — el blog post fundacional donde se argumenta el costo de Elasticsearch vs Loki. Reléanlo con los datos de su propio cluster en mano. https://grafana.com/blog/2018/12/12/loki-prometheus-inspired-open-source-logging-for-cloud-natives/
- Elastic — “Why Elasticsearch for logs” — la posición oficial de Elastic, con énfasis en full-text search y aggregations. Sirve para balancear el blog post de Grafana en el ADR. https://www.elastic.co/observability/log-monitoring