Skip to content

Instantly share code, notes, and snippets.

@egernst
Last active November 28, 2022 06:46
Show Gist options
  • Save egernst/d8f20021db724ba831a2552ba02027fe to your computer and use it in GitHub Desktop.
Save egernst/d8f20021db724ba831a2552ba02027fe to your computer and use it in GitHub Desktop.
ECK, Fluent-bit

todo:

  • - update Kibana object to set an antiaffinity (lack aarch64 support)
  • - show example of using fluent-bit annotation to highlight what parser to use.

EFK using fluent-bit and the Elastic Operator

ECK provides a higher baseline for security out of the box, which makes most "quick-start" guides for utilizing as a sink for logging fail. This gist provides details on how to update fluent-bit quick-start guides to work with ECK, utilizing emptyDir for the ES PVC.

The example below was tested with kind as well as on a single node baremetal cluster.

TL; DR: I don't care, let me apply the yaml:

ECK

kubectl apply -f https://download.elastic.co/downloads/eck/1.1.2/all-in-one.yaml
kubectl apply -f https://gist.githubusercontent.com/egernst/d8f20021db724ba831a2552ba02027fe/raw/4c41bb7f519b2f1fbde0a15d79fdcea9c9f59173/monitoring-elastic.yaml

Check that Kibana and ElasticSearch are healthy:

watch -d "kubectl get ElasticSearch,Kibana"

Interact with Kibana and Elastic require credentials. The user is elastic, and the password is kept within a secret. This can be obtained as follows:

PASSWORD=$(kubectl get secret quickstart-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 --decode)

Test that ES has the expected base indices:

kubectl port-forward service/quickstart-es-http 9200 & 
curl -u "elastic:$PASSWORD" -k "https://localhost:9200/_cat/indices?v&pretty"

Example output:

$ curl -u "elastic:$PASSWORD" -k "https://localhost:9200/_cat/indices?v&pretty"
health status index                    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   .security-7              uqpSrv44QsKnoHaz25wvRA   1   0         36            0     94.1kb         94.1kb
green  open   .kibana_task_manager_1   8UjmsswKRnOSR2LDmMLOJg   1   0          2            6     51.8kb         51.8kb
green  open   .apm-agent-configuration g_lHlwcaSfaZSwM7tnNWzQ   1   0          0            0       230b           230b
green  open   .kibana_1                etLB5pk1RzWWkxINn8mNYQ   1   0          1            0      5.7kb          5.7kb

Kibana should be available to access now through a local web browser

kubectl port-forward service/quickstart-kb-http 5601 & 
echo $PASSWORD
open https://localhost:5601

Fluent-bit

Setup the service account/CRDs:

Start the daemonset:

kubectl apply -f https://gist.githubusercontent.com/egernst/d8f20021db724ba831a2552ba02027fe/raw/e843dc1049adfc71cec49e7ca60ee73385b3b2fb/fluent-bit-role-sa.yaml
kubectl apply -f https://gist.githubusercontent.com/egernst/d8f20021db724ba831a2552ba02027fe/raw/79ef513c6c8eae656a039fe0ae9a466426e597f1/fluent-bit-configmap.yaml
kubectl apply -f https://gist.githubusercontent.com/egernst/d8f20021db724ba831a2552ba02027fe/raw/3c112c444bb41ab63fb8815337891baf5cfdc4cd/fluent-bit-ds.yaml

Take a look and make sure indices are updated to account for the fluent-bit output (ie, logstash):

$ curl -u "elastic:$PASSWORD" -k "https://localhost:9200/_cat/indices?v&pretty"
health status index                    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   logstash-2020.06.10      81IhKwVVR0KEPyJRABLYZg   1   1       4301            0      1.9mb          1.9mb
green  open   .security-7              uqpSrv44QsKnoHaz25wvRA   1   0         36            0     94.1kb         94.1kb
green  open   .kibana_task_manager_1   8UjmsswKRnOSR2LDmMLOJg   1   0          2            6     51.8kb         51.8kb
green  open   .apm-agent-configuration g_lHlwcaSfaZSwM7tnNWzQ   1   0          0            0       230b           230b
green  open   .kibana_1                etLB5pk1RzWWkxINn8mNYQ   1   0          1            0      5.7kb          5.7kb

Background

Describe some of the modifications/challenges I ran into when setting this up.

ECK Setup:

We start ElasticSearch and Kibana using the ECK operator. This is straightforward, though for ElasticSearch object, we use emptyDir for storage instead of a PVC for ease of setup.

In our case the resulting manifest for Elasticsearch and Kibana CRDs is standard, with the following change for Elastic:

---
apiVersion: elasticsearch.k8s.elastic.co/v1beta1
kind: Elasticsearch
spec:
  nodeSets:
    podTemplate:
      spec:
        volumes:
        - name: elasticsearch-data
          emptyDir: {}

Fluentd

It was a challenge to manage using a configmap, but also using environment variable to share the credential information for connecting to the ES from ECK. If I manually enter to the configmap this worked fine. Looking at the fluent-bit documents, it seems they have better support for managing secrets (ie, their config file can contain variables which can be pulled from the container's environment). With this, and given the improved efficiency, let's use fluent-bit instead.

Fluent-bit

Docs: elastic output params

The challenge when working with ECK, again, was using TLS and the appropriate credentials. Starting with the baseline example of elastic output in k8s configmap from here, we adjusted the output section to: include user/password, as well as TLS settings:

The configmap changes:

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  labels:
    k8s-app: fluent-bit
data:
   output-elasticsearch.conf: |
    [OUTPUT]
        Name            es
        Match           *
        Host            ${FLUENT_ELASTICSEARCH_HOST}
        Port            ${FLUENT_ELASTICSEARCH_PORT}
        HTTP_User       ${FLUENT_ELASTICSEARCH_USER}
        HTTP_Passwd     ${FLUENT_ELASTICSEARCH_PASSWORD}
        Logstash_Format On
        Replace_Dots    On
        Retry_Limit     False
        TLS             On
        TLS.verify      Off

The paser.conf needed to be updated to include the cri parser. CRI adds a timestamp/source information to each log entry. If you use docker parser, the original message will be 'globbed' with these additions. by utilizing the cri parser, the original log will be available in a message field. The parser we use is slightly modified from what is available on fluent-bit respository's parser.conf: we add Decode_Field json message . This was done in order to be able to parse logrus / json strucuted output automatically. Perhaps there's a way that we could chain parsers? In the meantime, the resulting addition:

    [PARSER]
        Name        cri
        Format      regex
        Regex       ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L%z
        Decode_Field json message

The [INPUT] stage of the configmap needed to be updated to leverage this cri parser, modifiying Parser from docker to cri.

The daemonset is based off of the example above, with the following additions for container variables:

apiVersion: apps/v1
kind: DaemonSet
spec:
  template:
    spec:
      containers:
        env:
        - name: FLUENT_ELASTICSEARCH_HOST
          value: "quickstart-es-http"
        - name: FLUENT_ELASTICSEARCH_PORT
          value: "9200"
        - name: FLUENT_ELASTICSEARCH_USER
          value: "elastic"
        - name: FLUENT_ELASTICSEARCH_PASSWORD
          valueFrom:
            secretKeyRef:
              name: quickstart-es-elastic-user
              key: elastic
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-config
labels:
k8s-app: fluent-bit
data:
# Configuration files: server, input, filters and output
# ======================================================
fluent-bit.conf: |
[SERVICE]
Flush 1
Log_Level info
Daemon off
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
@INCLUDE input-kubernetes.conf
@INCLUDE filter-kubernetes.conf
@INCLUDE output-elasticsearch.conf
input-kubernetes.conf: |
[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser cri
DB /var/log/flb_kube.db
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
filter-kubernetes.conf: |
[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
Kube_Tag_Prefix kube.var.log.containers.
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Parser On
K8S-Logging.Exclude Off
output-elasticsearch.conf: |
[OUTPUT]
Name es
Match *
Host ${FLUENT_ELASTICSEARCH_HOST}
Port ${FLUENT_ELASTICSEARCH_PORT}
HTTP_User ${FLUENT_ELASTICSEARCH_USER}
HTTP_Passwd ${FLUENT_ELASTICSEARCH_PASSWORD}
Logstash_Format On
Replace_Dots On
Retry_Limit False
TLS On
TLS.verify Off
parsers.conf: |
[PARSER]
Name apache
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache2
Format regex
Regex ^(?<host>[^ ]*) [^ ]* (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^ ]*) +\S*)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name apache_error
Format regex
Regex ^\[[^ ]* (?<time>[^\]]*)\] \[(?<level>[^\]]*)\](?: \[pid (?<pid>[^\]]*)\])?( \[client (?<client>[^\]]*)\])? (?<message>.*)$
[PARSER]
Name nginx
Format regex
Regex ^(?<remote>[^ ]*) (?<host>[^ ]*) (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name json
Format json
Time_Key time
Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L
Time_Keep On
[PARSER]
Name cri
Format regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>[^ ]*) (?<message>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
Decode_Field json message
[PARSER]
Name syslog
Format regex
Regex ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$
Time_Key time
Time_Format %b %d %H:%M:%S
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
labels:
k8s-app: fluent-bit-logging
version: v1
kubernetes.io/cluster-service: "true"
spec:
selector:
matchLabels:
k8s-app: fluent-bit-logging
version: v1
template:
metadata:
labels:
k8s-app: fluent-bit-logging
version: v1
kubernetes.io/cluster-service: "true"
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "2020"
prometheus.io/path: /api/v1/metrics/prometheus
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:1.3.11
imagePullPolicy: Always
ports:
- containerPort: 2020
env:
- name: FLUENT_ELASTICSEARCH_HOST
value: "quickstart-es-http"
- name: FLUENT_ELASTICSEARCH_PORT
value: "9200"
- name: FLUENT_ELASTICSEARCH_USER
value: "elastic"
- name: FLUENT_ELASTICSEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: quickstart-es-elastic-user
key: elastic
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: fluent-bit-config
mountPath: /fluent-bit/etc/
terminationGracePeriodSeconds: 10
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: fluent-bit-config
configMap:
name: fluent-bit-config
serviceAccountName: fluent-bit
tolerations:
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
- operator: "Exists"
effect: "NoExecute"
- operator: "Exists"
effect: "NoSchedule"
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: fluent-bit-read
rules:
- apiGroups: [""]
resources:
- namespaces
- pods
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: fluent-bit-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: fluent-bit-read
subjects:
- kind: ServiceAccount
name: fluent-bit
namespace: default
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluent-bit
---
apiVersion: elasticsearch.k8s.elastic.co/v1beta1
kind: Elasticsearch
metadata:
name: quickstart
spec:
version: 7.5.0
nodeSets:
- count: 1
name: all-nodes
config:
node.master: true
node.data: true
node.ingest: true
node.store.allow_mmap: false
podTemplate:
spec:
volumes:
- name: elasticsearch-data
emptyDir: {}
---
apiVersion: kibana.k8s.elastic.co/v1beta1
kind: Kibana
metadata:
name: quickstart
spec:
version: 7.5.0
count: 1
elasticsearchRef:
name: quickstart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment