Log aggregation for Kubernetes using Elasticsearch
Jonathan Seth Mainguy
Engineer @ Bandwidth
July 18th, 2019
Why should we listen to you?
I am on the Systems Platform team at Bandwidth.
Part of our teams role is managing the Elastic Stack our internal customers use.
Another one of our teams roles is managing the openshift clusters the business uses.
Whats the problem?
We wanted a single location for all our logs and metrics for vms, physical boxes, and kubernetes clusters.
This gives us, and our internal customers a familiar experience when debugging issues.
Whats Elastic Stack?
Elasticsearch, Kibana, Beats, and Logstash
Most of which is open source and can be used for free.
Elastic provides paid support, service, and a few additional features that are paid only.
Elasticsearch
Elasticsearch is an open-source, RESTful, distributed search and analytics engine built on Apache Lucene
Elasticsearch
You can send data in the form of JSON documents to Elasticsearch using the API or ingestion tools such as Logstash.
You can then search and retrieve the document using the Elasticsearch API or a pretty UI like Kibana.
Kibana
Kibana is a pretty UI. For our internal users, this is the only part of the stack they pay attention to.
It makes searching much easier to use and understand than trying to GET to the api by hand.
You can make pretty graphs in it as well.
Beats
Not just overpriced mediocre headphones.
Beats - Gather data and ship to Elasticsearch (or a buffer like Kafka if you prefer)
Beats
Logstash
server-side data processing pipeline that ingests data, transforms it, and then sends it to your favorite "stash."
Ingest
We use it to get syslog from appliances into Kafka, similar to how our beats for products that support it, send to Kafka.
Ingest and Transform
We also use it to ingest all data from kafka, and optionally "drop" namespaces from being logged.
Logstash
Send
After ingesting and transforming from Kafka, we finally send the data to our Elasticsearch servers.
Kafka
Not part of the Elasticsearch offering, but a big part of our Elastic Stack flow.
Apache Kafka is a community distributed event streaming platform capable of handling trillions of events a day. Initially conceived as a messaging queue.
We use it as a buffer between beats and elasticsearch, also provides us with disaster recovery via its replay ability.
Complicated
there are a lot of pieces, that can be complex to understand and manage
/etc/sysconfig/docker on the K8 nodes
# /etc/sysconfig/docker
# Modify these options if you want to change the way the docker daemon runs
OPTIONS=' --selinux-enabled --log-driver=json-file --log-opt max-size=10m
/var/lib/docker/containers/long-random-uid-json.log
Generates json logs to a location like
Filebeat Daemonset
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: filebeat
namespace: filebeat
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
spec:
template:
metadata:
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
spec:
serviceAccountName: filebeat
terminationGracePeriodSeconds: 30
containers:
- name: filebeat
image: docker.elastic.co/beats/filebeat:6.5.1
args: [
"-c", "/etc/filebeat.yml",
"-e",
]
Filebeat Daemonset
securityContext:
runAsUser: 0
privileged: true
resources:
limits:
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: config
mountPath: /etc/filebeat.yml
readOnly: true
subPath: filebeat.yml
- name: prospectors
mountPath: /usr/share/filebeat/prospectors.d
readOnly: true
- name: data
mountPath: /usr/share/filebeat/data
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
Filebeat Daemonset
volumes:
- name: config
configMap:
defaultMode: 0600
name: filebeat-config
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: prospectors
configMap:
defaultMode: 0600
name: filebeat-prospectors
- name: data
emptyDir: {}
filebeat.yml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: filebeat
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
data:
filebeat.yml: |-
logging.json: true
filebeat.config:
filebeat.autodiscover:
providers:
- type: kubernetes
hints.enabled: true
inputs:
# Mounted `filebeat-prospectors` configmap:
path: ${path.config}/prospectors.d/*.yml
# Reload prospectors configs as they change:
reload.enabled: false
modules:
path: ${path.config}/modules.d/*.yml
# Reload module configs as they change:
reload.enabled: false
output.kafka:
hosts: ["{{ kafka_hosts }}"]
topic: "beats_filebeat"
version: 0.10.1
username: "{{ filbeatproducer_user" }}
password: "{{ filebeatproducer_secret }}"
Filebeat Prospector
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-prospectors
namespace: filebeat
labels:
k8s-app: filebeat
kubernetes.io/cluster-service: "true"
data:
kubernetes.yml: |-
- type: docker
containers.ids:
- "*"
processors:
- add_kubernetes_metadata:
in_cluster: true
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
json.overwrite_keys: true
json.ignore_decoding_error: true
fields:
kubernetes.cluster.site: {{ subdomain }}
fields_under_root: true
scan_frequency: 1s
Ansible to deploy
- name: Apply yamls
k8s:
verify_ssl: false
state: present
definition: "{{ item }}"
with_items:
- "{{ lookup('template', 'filebeat-config.yaml') }}"
- "{{ lookup('template', 'filebeat-prospectors.yaml') }}"
- "{{ lookup('file', 'filebeat-cluster-role-binding.yaml') }}"
- "{{ lookup('file', 'filebeat-cluster-role.yaml') }}"
- "{{ lookup('file', 'filebeat-daemonset.yaml') }}"
- "{{ lookup('file', 'filebeat-service-account.yaml') }}"
no_log: true
How Filebeat collects logs on Kubernetes
Docker outputs logs to that long directory.
Filebeat pods run in a Daemonset, one per node
They run as a privileged pod that volume mounts directories from the Hosts filesystem.
Filebeat then scans for these logs once a second, and ships their contents as documents to Kafka.
Logstash - Indexer
apiVersion: apps/v1
kind: Deployment
metadata:
name: ls-indexer-filebeat
namespace: ls-indexer
spec:
replicas: 15
revisionHistoryLimit: 2
selector:
matchLabels:
app: ls-indexer-filebeat
strategy:
activeDeadlineSeconds: 21600
resources: {}
rollingParams:
intervalSeconds: 1
maxSurge: 25%
maxUnavailable: 25%
timeoutSeconds: 600
updatePeriodSeconds: 1
type: RollingUpdate
template:
metadata:
creationTimestamp: null
labels:
app: ls-indexer-filebeat
Logstash - Indexer
spec:
containers:
- args:
- -c
- bin/logstash-plugin install logstash-filter-prune && bin/logstash
command:
- /bin/sh
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
image: docker.elastic.co/logstash/logstash:6.5.4
imagePullPolicy: IfNotPresent
name: ls-indexer-filebeat
resources:
limits:
cpu: "2"
memory: 2000Mi
requests:
cpu: "1"
memory: 1000Mi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
Logstash - Indexer
volumeMounts:
- mountPath: /usr/share/logstash/pipeline/logstash.conf
name: config-filebeat
readOnly: true
subPath: filebeat.conf
- mountPath: /usr/share/logstash/jaas.conf
name: config-jaas
readOnly: true
subPath: jaas.conf
- mountPath: /usr/share/logstash/ca.crt
name: config-ca-crt
readOnly: true
subPath: ca.crt
- mountPath: /usr/share/logstash/config/logstash.yml
name: config-logstash-yml
readOnly: true
subPath: logstash.yml
- mountPath: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/logstash-patterns-core-4.1.2/patterns/grok-custom-patterns
name: grok-custom-patterns
readOnly: true
subPath: grok-custom-patterns
Logstash - Indexer
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- configMap:
defaultMode: 384
name: config-logstash-yml
name: config-logstash-yml
- configMap:
defaultMode: 384
name: config-filebeat
name: config-filebeat
- configMap:
defaultMode: 384
name: config-jaas
name: config-jaas
- configMap:
defaultMode: 384
name: config-ca-crt
name: config-ca-crt
- configMap:
defaultMode: 384
name: grok-custom-patterns
name: grok-custom-patterns
Logstash - Filebeat index - conf
apiVersion: v1
data:
filebeat.conf: |
input {
kafka {
consumer_threads => 1
topics => ["beats_filebeat"]
bootstrap_servers => "{{ logstash_bootstrap_servers }}"
sasl_mechanism => "PLAIN"
security_protocol => "SASL_PLAINTEXT"
jaas_path => "/usr/share/logstash/jaas.conf"
codec => "json"
add_field => {
"[@metadata][logtype]" => "filebeat"
}
}
}
Logstash - Filebeat index - conf
filter {
if [@metadata][logtype] == "filebeat" and ("noisy-neighbor" in [kubernetes][namespace]) {
drop { }
}
output {
if "index" in [fields] {
elasticsearch {
hosts => {{ logstash_elasticsearch_url }}
user => {{ logstash_elasticsearch_username }}
cacert => "/usr/share/logstash/ca.crt"
password => "{{ logstash_elasticsearch_password }}"
index => "%{[fields][index]}-%{[beat][version]}-%{+YYYY.MM.dd}"
manage_template => false
}
}
Metricbeat conf
---
# Deploy a Metricbeat instance per node for node metrics retrieval
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: metricbeat
namespace: metricbeat
labels:
k8s-app: metricbeat
spec:
template:
metadata:
labels:
k8s-app: metricbeat
spec:
serviceAccountName: metricbeat
terminationGracePeriodSeconds: 30
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: metricbeat
image: docker.elastic.co/beats/metricbeat:6.5.1
args: [
"-c", "/etc/metricbeat.yml",
"-e",
"-system.hostfs=/hostfs",
]
Metricbeat conf
---
# Deploy a Metricbeat instance per node for node metrics retrieval
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: metricbeat
namespace: metricbeat
labels:
k8s-app: metricbeat
spec:
template:
metadata:
labels:
k8s-app: metricbeat
spec:
serviceAccountName: metricbeat
terminationGracePeriodSeconds: 30
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: metricbeat
image: docker.elastic.co/beats/metricbeat:6.5.1
args: [
"-c", "/etc/metricbeat.yml",
"-e",
"-system.hostfs=/hostfs",
]
Metricbeat conf
securityContext:
runAsUser: 0
privileged: true
resources:
limits:
cpu: 200m
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: config
mountPath: /etc/metricbeat.yml
readOnly: true
subPath: metricbeat.yml
- name: modules
mountPath: /usr/share/metricbeat/modules.d
readOnly: true
- name: dockersock
mountPath: /var/run/docker.sock
- name: proc
mountPath: /hostfs/proc
readOnly: true
- name: cgroup
mountPath: /hostfs/sys/fs/cgroup
readOnly: true
Metricbeat conf
volumes:
- name: proc
hostPath:
path: /proc
- name: cgroup
hostPath:
path: /sys/fs/cgroup
- name: dockersock
hostPath:
path: /var/run/docker.sock
- name: config
configMap:
defaultMode: 0600
name: metricbeat-daemonset-config
- name: modules
configMap:
defaultMode: 0600
name: metricbeat-daemonset-modules
- name: data
hostPath:
path: /var/lib/metricbeat-data
type: DirectoryOrCreate
Metricbeat.yml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: metricbeat-deployment-modules
namespace: metricbeat
labels:
k8s-app: metricbeat
data:
# This module requires `kube-state-metrics` up and running under `kube-system` namespace
kubernetes.yml: |-
- module: kubernetes
metricsets:
- state_node
- state_deployment
- state_replicaset
- state_pod
- state_container
period: 10s
host: ${NODE_NAME}
hosts: ["kube-state-metrics.openshift-monitoring.svc:8443"]
Metricbeat.yml
# Kubernetes events
- module: kubernetes
enabled: true
metricsets:
- event
- module: kubernetes
enabled: true
metricsets:
- apiserver
hosts: ["https://kubernetes.default.svc"]
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
ssl.certificate_authorities:
- /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
Gets us kewl metrics like
Openshift 3.x ships with a built in elastic stack
It creates an index per namespace, in our use case this became unmanageable with thousands of index's very quickly.
However it is an easy way to try out elastic and get a feel for some of the components.
Elastic has a K8's operator for standing up a stack.
Pain Points
elasticsearch isnt optimized for deleting documents.
Sam wrote tooling to delete specific documents for us, takes 12 hours to delete 100M docs
We have one filebeat index per day, in our lab this sits around 385 Million documents on average.
Elastic scales (cuz it's stretchy)
So far we have moved our logstash indexers into kubernetes, they had previously been dedicated VMs.
This allows us to set our deployment to have one logstash pod per kafka topic partition, to speed up our consumption of documents.
Elastic scales (cuz it's stretchy)
All the parts of Elastic would work well in kubernetes.
Elasticsearch and Kibana are java apps.
We like having bare metal boxes for our elastic data to live on, however we can move kibana and ls-shipper to kubernetes when we have some time to do so.
Elastic scales (cuz it's stretchy)
Shards?
We aim to keep each shard below 50G and create the amount of shards per index based on this.
Questions?
Company I work at: https://www.bandwidth.com/
Bandwidth is hiring for lots of tech roles, one of which is a role for someone with "elasticsearch / lucene / kakfa" experience.
My referral link
https://grnh.se/6nykfp1