CLOUD & MANAGED SERVICESKUBERNETES CLOUD
02/12/2022 • Ugur Akkar

How To Deploy an EFK Stack to Kubernetes with xpack security

In this Kubernetes tutorial, you will learn how to setup an EFK stack on Kubernetes with xpack security feature turned on for log streaming, log analysis and log monitoring. 

When running multiple applications and services on a Kubernetes cluster, it makes more sense to stream all of your Kubernetes Cluster logs to one centralized logging infrastructure for easy log analysis. This can help you quickly sort through and analyse the heavy volume of log data produced by your Pods. By enabling xpack security feature, you can create and manage: users, roles, views, .... . This gives the possibility to give certain permission to view, edit, create dashboard, ... for a subset of application logs (indexes). 

One popular centralized logging solution is the Elasticsearch, Fluentd, and Kibana (EFK) stack.

What does each component do?

  • Elasticsearch: captures incoming data and stores in Indexes.
  • Fluentd: tails applications in your cluster and sends it directly to Elasticsearch.
  • Kibana: makes it possible to view logs, run queries, create own dashboard, … from Elasticsearch Indexes (data).

Elastic recently stated that security features are distributed with the basic license by default.

Elastic released some security features for free as part of the default distribution (Basic license) starting in Elastic Stack 6.8 and 7.1. This new feature offering includes the ability to encrypt network traffic using SSL, create and manage users, define roles that protect index and cluster-level access and fully secure Kibana.

1. Prerequisites

Before we can begin with this guide, ensure you have the following things available to you:

  • A Kubernetes 1.10+ cluster with role-based access control (RBAC) enabled.
  • The kubectl command-line tool installed on your local machine, configured to connect to your cluster.
  • (Optional) SealedSecret Controller deployed to the cluster and kubeseal installed on your local machine.

Once you have these components set up, you are ready to begin with this guide. Let's go!

2. Creating the Namespaces

Let's begin with creating the necessary namespaces for each application.

Elasticsearch namespace:

kind: Namespace
apiVersion: v1
metadata:
  name: elasticsearch
  group: elasticsearch

FluentD namespace:

kind: Namespace
apiVersion: v1
metadata:
  name: fluentd
  group: fluentd

Kibana namespace:

kind: Namespace
apiVersion: v1
metadata:
  name: fluentd
  group: fluentd

Once we have created the yaml files, we can deploy the yaml files to the cluster:

kubectl create -f elasticsearch.yaml -f kibana.yaml -f fluentd.yaml 

Following output should appear:

namespace/elasticsearch created
namespace/kibana created
namespace/fluentd created

We can validate if the namespaces are successfully created by running the following command:

kubectl get namespaces

The following output should appear:

NAME           STATUS    AGE
default        Active    15d
kube-system    Active    15d
elasticsearch  Active    1m
kibana         Active    1m
fluentd        Active    1m

3.  Deploying Elasticsearch Statefulset

First we need to deploy Elasticsearch. Elasticsearch is the core component in the stack, Fluentd and Kibana can not work without ElasticSearch.

You can find more information about Elasticsearch by clicking this link: https://www.elastic.co/what-is/elasticsearch

3.1 Creating ServiceAccount

Let's first start with creating the RBAC resources. We will give the Elasticsearch ServiceAccount enough permission to explore the cluster and search for other Elasticsearch nodes.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: elasticsearch
  namespace: elasticsearch
  labels:
    app: elasticsearch

We have our ServiceAccount, now we need to create the ClusterRole and bind it to the elasticsearch ServiceAccount.

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: elasticsearch
  labels:
    k8s-app: elasticsearch
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
  - ""
  resources:
  - "services"
  - "namespaces"
  - "endpoints"
  verbs:
  - "get"

Binding it to the ServiceAccount.

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: elasticsearch
  name: elasticsearch
  labels:
    k8s-app: elasticsearch
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
  name: elasticsearch
  namespace: elasticsearch
  apiGroup: ""
roleRef:
  kind: ClusterRole
  name: elasticsearch
  apiGroup: ""

3.2 Creating Headless Services

For the next step, we need a Service resource in the cluster. We will create a Headless Service resource with name elasticsearch in the namespace elasticsearch. When we associate our Elasticsearch StatefulSet with this Service, the Service will return DNS A records (service-name.namespace.svc.cluster.local) from that point to Elasticsearch Pods with the app: elasticsearch label. We will later configure these DNS records to our Statefulset, so Elasticsearch will search for these nodes.

apiVersion: v1
kind: Service
metadata:
  name: elasticsearch
  namespace: elasticsearch
  labels:
    app: elasticsearch
spec:
  selector:
    app: elasticsearch
  ports:
    - name: rest
      port: 9200
      targetPort: 9200
    - name: transport
      port: 9300
      targetPort: 9300

Let's deploy our yaml files to the cluster:

kubectl create -f Service.yaml -f ServiceAccount.yaml -f ClusterRole.yaml -f ClusterRoleBinding.yaml

Now let's see if the elasticsearch Service is deployed successfully:

kubectl get services -n elasticsearch

Following output should appear:

NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
elasticsearch   ClusterIP   None         <none>        9200/TCP,9300/TCP   1m

3.3 Creating Statefulset

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: elasticsearch
spec:
  serviceName: elasticsearch
  replicas: 3
  updateStrategy:
    type: OnDelete
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      securityContext:
        fsGroup: 1000
      initContainers:
        - name: increase-vm-max-map
          image: busybox
          imagePullPolicy: IfNotPresent
          securityContext:
            privileged: true
          command: [ "sysctl", "-w", "vm.max_map_count=262144" ]
      containers:
        - name: elasticsearch
          image: docker.elastic.co/elasticsearch/elasticsearch:8.3.2
          env:
            - name: node.name
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NODE_MASTER
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NODE_MASTER
            - name: NODE_DATA
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NODE_DATA
            - name: NUMBER_OF_MASTERS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NUMBER_OF_MASTERS
            - name: NUMBER_OF_REPLICAS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NUMBER_OF_REPLICAS
            - name: ES_JAVA_OPTS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: ES_JAVA_OPTS
            - name: ES_PORT
              value: "9200"
            - name: ELASTIC_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: elastic-credentials
                  key: ELASTIC_PASSWORD
          ports:
            - containerPort: 9200
              name: rest
              protocol: TCP
            - containerPort: 9300
              name: transport
              protocol: TCP
          volumeMounts:
            - name: elasticsearch-data
              mountPath: /usr/share/elasticsearch/data
            - name: elasticsearch-yml
              mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
              subPath: elasticsearch.yml
        resources:
            requests:
                cpu: "1000m"
                memory: "2Gi"
            limits:
                cpu: "1000m"
                memory: "2Gi"      
        volumes:
        - name: elasticsearch-yml
          configMap:
            name: elasticsearch-config
            items:
              - key: elasticsearch.yml
                path: elasticsearch.yml
  volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
        annotations:
          volume.beta.kubernetes.io/storage-class: gp3
      spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: 50Gi

We have defined some environment variables in our Statefulset resources. Some of the variables are from ConfigMap and some from a Secret.

Secret contains the password of the Elasticsearch admin user.

Run the following command to create a yaml file for the elasticsearch admin password:

# Create SealedSecret for the admin elasticsearch password
kubectl -n elasticsearch create secret generic elastic-credentials \  
  --from-literal=ELASTIC_PASSWORD='STRONG-PASSWORD' \
  --dry-run=client -o yaml | ${KUBESEAL_BINARY} --cert ${KUBESEAL_CERT_PATH} --format yaml > SealedSecret-ElasticCredentials.yaml

If you don't have a SealedSecret controller, you can make a Secret resource by running the following command:

# Create SealedSecret for the admin elasticsearch password
kubectl -n elasticsearch create secret generic elastic-credentials \  
  --from-literal=ELASTIC_PASSWORD='STRONG-PASSWORD' \
  --dry-run=client -o yaml > SealedSecret-ElasticCredentials.yaml

The command above will create the yaml file that needs to be deployed to the cluster.

3.4  Creating ConfigMap

The Configmap contains elasticsearch.yml block with extra Elasticsearch configuration. We add our Service DNS records to our discovery.seed_hosts, Elasticsearch will search for additional nodes. 

This block will be mounted on the pod under /usr/share/elasticsearch/config/elasticsearch.yml location.

apiVersion: v1
kind: ConfigMap
metadata:
  name: elasticsearch-config
  namespace: elasticsearch
data:
  elasticsearch.yml: |
    cluster.name: "elasticsearch"
    bootstrap.memory_lock: false
    xpack.license.self_generated.type: basic
    network.host: "0.0.0.0"
    logger.org.elasticsearch.transport: error
    logger.org.elasticsearch.discovery: error
    discovery.seed_hosts:
       - elasticsearch-0.elasticsearch.elasticsearch.svc.cluster.local:9300
       - elasticsearch-1.elasticsearch.elasticsearch.svc.cluster.local:9300
       - elasticsearch-2.elasticsearch.elasticsearch.svc.cluster.local:9300
    cluster.initial_master_nodes:
       - elasticsearch-0
       - elasticsearch-1
       - elasticsearch-2
  NODE_MASTER: "true"
  NODE_DATA: "true"
  NUMBER_OF_MASTERS: "3"
  NUMBER_OF_REPLICAS: "2"

This volume mount is also declared in statefulset.yaml:

          volumeMounts:
            - name: elasticsearch-data
              mountPath: /usr/share/elasticsearch/data
            - name: elasticsearch-yml
              mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
              subPath: elasticsearch.yml

Deploy all your yaml files and make sure that Elasticsearch is running without any problems. If Elasticsearch is not running properly, you can tail the container logs or describe the Pod/Statefulset. 

kubectl create -f ConfigMap.yaml -f Statefulset.yaml

The following output should appear:

configmap/elasticsearch-config created
statefulset/elasticsearch created

Let's see if the elasticsearch statefulset is deployed successfully.

kubectl get pod -n elasticsearch

Following output should appear:

NAMESPACE       NAME                                         READY   STATUS      RESTARTS       AGE
elasticsearch   elasticsearch-0                              1/1     Running     0              2m
elasticsearch   elasticsearch-1                              1/1     Running     0              1m
elasticsearch   elasticsearch-2                              1/1     Running     0              30sc

3.5 Enabling xpack feature

3.5.1 Generate certificates

Elasticsearch will fail to start when the security feature is ON without security configuration is configured!

Before we can enable the security feature, we need to generate certificates for elasticsearch nodes. Elasticsearch nodes will communicate securely with each other. 

Run the following commands in the elasticsearch container.

kubectl -n elasticsearch exec -ti elasticsearch-0 -- bash
 
# Create certificates
elasticsearch-certutil ca --out /tmp/elastic-stack-ca.p12 --pass ''
elasticsearch-certutil cert --name security-master --dns security-master --ca /tmp/elastic-stack-ca.p12 --pass '' --ca-pass '' --out /tmp/elastic-certificates.p12
 
# copy certificates to local machine
sudo kubectl cp elasticsearch/elasticsearch-0:/tmp/elastic-stack-ca.p12 ./elastic-stack-ca.p12
sudo kubectl cp elasticsearch/elasticsearch-0:/tmp/elastic-certificates.p12 ./elastic-certificates.p12
 
# Validate and extract PEM
openssl pkcs12 -nodes -passin pass:'' -in elastic-certificates.p12 -out elastic-certificate.pem

Once we have generated our certificate and copied it from the container to our local machine, we will create a SealedSecret from the PEM file. We will mount this PEM file to the container later.

# Create SealedSecret for the P12 file
kubectl -n elasticsearch create secret generic elastic-certificate-pem \
  --from-file=elastic-certificates.p12 \
  --dry-run=client -o yaml | ${KUBESEAL_BINARY} --cert ${KUBESEAL_CERT_PATH} --format yaml > SealedSecret-ElasticCertificates.yaml

If you don't have SealedSecret controller, you can make a Secret resource by running the following command.

# Create SealedSecret for the P12 file
kubectl -n elasticsearch create secret generic elastic-certificate-pem \
  --from-file=elastic-certificates.p12 \
  --dry-run=client -o yaml > SealedSecret-ElasticCertificates.yaml

The command above will create the yaml file that needs to be deployed to the cluster. 

3.5.2 Enable xpack security features

When you have successfully created and deployed your certificate to the cluster, we can now enable security features.

Add the following configuration to the elasticsearch.yml configuration in the ConfigMap.yaml file:

    xpack.license.self_generated.type: basic
    xpack.security.enabled: true
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: certificate
    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.http.ssl.enabled: false
    xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12

Setting xpack.security.enabled to true will enable xpack security features. But only enabling this setting is not enough. We also need to mount and configure our newly generated certificates.

Mount the secret that contains the certificates to the StatefulSet:

          volumeMounts:
            - name: elasticsearch-data
              mountPath: /usr/share/elasticsearch/data
            - name: elasticsearch-yml
              mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
              subPath: elasticsearch.yml
            - name: elastic-certificates
              mountPath: /usr/share/elasticsearch/config/certs
           
          .....
          .....
       
      volumes:
        - name: elasticsearch-yml
          configMap:
            name: elasticsearch-config
            items:
              - key: elasticsearch.yml
                path: elasticsearch.yml
        - name: elastic-certificates
          secret:
            secretName: elastic-certificates

Save and replace the ConfigMap and Statefulset. Wait till all pods have been terminated and started again.
if the pods are not restarted automatically, scale down statefulset and scale back up:

kubectl -n elasticsearch scale statefulset elasticsearch --replicas 0
#wait till all nodes are deleted
 
kubectl -n elasticsearch scale statefulset elasticsearch --replicas 3

Tail logs and make sure that Elasticsearch is running healthy. If Elasticsearch is not running properly, you can tail the container logs or describe the Pod/Statefulset. 

Your ConfigMap.yaml and Statefulset.yaml file should look like this.

3.5.3 Statefulset

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: elasticsearch
  namespace: elasticsearch
spec:
  serviceName: elasticsearch
  replicas: 3
  updateStrategy:
    type: OnDelete
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    spec:
      securityContext:
        fsGroup: 1000
      initContainers:
        - name: increase-vm-max-map
          image: busybox
          imagePullPolicy: IfNotPresent
          securityContext:
            privileged: true
          command: [ "sysctl", "-w", "vm.max_map_count=262144" ]
      containers:
        - name: elasticsearch
          image: "defined_in_kustomization"
          env:
            - name: node.name
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NODE_MASTER
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NODE_MASTER
            - name: NODE_DATA
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NODE_DATA
            - name: NUMBER_OF_MASTERS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NUMBER_OF_MASTERS
            - name: NUMBER_OF_REPLICAS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: NUMBER_OF_REPLICAS
            - name: ES_JAVA_OPTS
              valueFrom:
                configMapKeyRef:
                  name: elasticsearch-config
                  key: ES_JAVA_OPTS
            - name: ES_PORT
              value: "9200"
            - name: ELASTIC_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: elastic-credentials
                  key: ELASTIC_PASSWORD
          ports:
            - containerPort: 9200
              name: rest
              protocol: TCP
            - containerPort: 9300
              name: transport
              protocol: TCP
          volumeMounts:
            - name: elasticsearch-data
              mountPath: /usr/share/elasticsearch/data
            - name: elasticsearch-yml
              mountPath: /usr/share/elasticsearch/config/elasticsearch.yml
              subPath: elasticsearch.yml
            - name: elastic-certificates
              mountPath: /usr/share/elasticsearch/config/certs
          resources:
            requests:
              cpu: "1000m"
              memory: "2Gi"
            limits:
              cpu: "1000m"
              memory: "2Gi"
      volumes:
        - name: elasticsearch-yml
          configMap:
            name: elasticsearch-config
            items:
              - key: elasticsearch.yml
                path: elasticsearch.yml
        - name: elastic-certificates
          secret:
            secretName: elastic-certificates
  volumeClaimTemplates:
    - metadata:
        name: elasticsearch-data
        annotations:
          volume.beta.kubernetes.io/storage-class: gp3
      spec:
        accessModes: [ "ReadWriteOnce" ]
        resources:
          requests:
            storage: 50Gi
3.5.3.1 readinessProbe

If you would like to add readinessprobe, add the following to your Statefulset.yaml:

readinessProbe:
 exec:
   command:
     - /bin/bash
     - -c
     - |-
       health=$(curl -s -o /dev/null -u elastic:${ELASTIC_PASSWORD} --write-out "%{http_code}"  localhost:9200/_cluster/health?local=true)
       if [[ ${health} -ne 200 ]]; then exit 1; fi
 initialDelaySeconds: 5

3.5.4 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: elasticsearch-config
  namespace: elasticsearch
data:
  elasticsearch.yml: |
    cluster.name: "elasticsearch"
    bootstrap.memory_lock: false
    xpack.license.self_generated.type: basic
    xpack.monitoring.collection.enabled: true
    xpack.security.http.ssl.enabled: false
    xpack.security.transport.ssl.enabled: true
    xpack.security.transport.ssl.verification_mode: certificate
    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
    network.host: "0.0.0.0"
    logger.org.elasticsearch.transport: error
    logger.org.elasticsearch.discovery: error
    discovery.seed_hosts:
       - elasticsearch-0.elasticsearch.elasticsearch.svc.cluster.local:9300
       - elasticsearch-1.elasticsearch.elasticsearch.svc.cluster.local:9300
       - elasticsearch-2.elasticsearch.elasticsearch.svc.cluster.local:9300
    cluster.initial_master_nodes:
       - elasticsearch-0
       - elasticsearch-1
       - elasticsearch-2
  NODE_MASTER: "true"
  NODE_DATA: "true"
  NUMBER_OF_MASTERS: "3"
  NUMBER_OF_REPLICAS: "2"
  ES_JAVA_OPTS: "-Djava.net.preferIPv4Stack=true -Xms1750m -Xmx1750m"

4. Deploying Fluentd DaemonSet

Now it is time to send container logs to Elasticsearch. We already created our fluentD namespace.

You can find more information about FluentD by clicking on the following link: https://www.fluentd.org/

4.1 Creating ServiceAccount

Let's start again with creating the RBAC resources. We will give the FluentD ServiceAccount enough permission to explore the cluster and tail container logs.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: fluentd
  labels:
    app: fluentd

Next, ClusterRole and bind it to the fluentd ServiceAccount.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd
  labels:
    app: fluentd
rules:
  - apiGroups:
      - ""
    resources:
      - pods
      - namespaces
    verbs:
      - get
      - list
      - watch

Now bind it to the ServiceAccount.

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: fluentd
roleRef:
  kind: ClusterRole
  name: fluentd
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: fluentd
    namespace: fluentd

4.2 Creating ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: fluentd
data:
  fluent.conf: |
    <match fluent.**>
        # this tells fluentd to not output its log on stdout
        @type null
    </match>
    # here we read the logs from Docker's containers and parse them
    <source>
      @type tail
      path /var/log/containers/*.log
      pos_file /var/log/app.log.pos
      tag kubernetes.*
      read_from_head true
      <parse>
        @type json
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      </parse>
    </source>
    # we use kubernetes metadata plugin to add metadatas to the log
    <filter kubernetes.**>
        @type kubernetes_metadata
    </filter>
     # we send the logs to Elasticsearch
    <match **>
       @type elasticsearch_dynamic
       @log_level info
       include_tag_key true
       host "#{ENV['FLUENT_ELASTICSEARCH_HOST']}"
       port "#{ENV['FLUENT_ELASTICSEARCH_PORT']}"
       user "#{ENV['FLUENT_ELASTICSEARCH_USER']}"
       password "#{ENV['FLUENT_ELASTICSEARCH_PASSWORD']}"
       scheme "#{ENV['FLUENT_ELASTICSEARCH_SCHEME'] || 'http'}"
       ssl_verify "#{ENV['FLUENT_ELASTICSEARCH_SSL_VERIFY'] || 'true'}"
       reload_connections true
       logstash_format true
       logstash_prefix "#{ENV['K8S_NODE_NAME']}-${record['kubernetes']['pod_name']}"
       <buffer>
           @type file
           path /var/log/fluentd-buffers/kubernetes.system.buffer
           flush_mode interval
           retry_type exponential_backoff
           flush_thread_count 2
           flush_interval 5s
           retry_forever true
           retry_max_interval 30
           chunk_limit_size 2M
           queue_limit_length 32
           overflow_action block
       </buffer>
    </match>
  K8S_NODE_NAME: "efk-stack"
  FLUENT_ELASTICSEARCH_USER: "elastic"
  NUMBER_OF_REPLICAS: "2"
  FLUENT_ELASTICSEARCH_HOST: "elasticsearch.elasticsearch.svc.cluster.local"

4.3 Creating DaemonSet

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd
  namespace: fluentd
  labels:
    app: fluentd
spec:
  selector:
    matchLabels:
      app: fluentd
  template:
    metadata:
      labels:
        app: fluentd
    spec:
      serviceAccountName: fluentd
      containers:
        - name: fluentd
          image: "defined_in_kustomization"
          env:
            - name: FLUENT_ELASTICSEARCH_HOST
              valueFrom:
                configMapKeyRef:
                  name: fluentd-config
                  key: FLUENT_ELASTICSEARCH_HOST
            - name: FLUENT_ELASTICSEARCH_PORT
              value: "9200"
            - name: FLUENT_ELASTICSEARCH_SCHEME
              value: "http"
            - name: FLUENTD_SYSTEMD_CONF
              value: disable
            - name: K8S_NODE_NAME
              valueFrom:
                configMapKeyRef:
                  name: fluentd-config
                  key: K8S_NODE_NAME
            - name: FLUENT_ELASTICSEARCH_USER
              valueFrom:
                configMapKeyRef:
                  name: fluentd-config
                  key: FLUENT_ELASTICSEARCH_USER
            - name: FLUENT_ELASTICSEARCH_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: fluentd-credentials
                  key: FLUENT_ELASTICSEARCH_PASSWORD
          resources:
            limits:
              memory: 512Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:
            - name: varlog
              mountPath: /var/log
            - name: varlibdockercontainers
              mountPath: /var/lib/docker/containers
              readOnly: true
            - name: fluentd-config
              mountPath: /fluentd/etc
      terminationGracePeriodSeconds: 30
      volumes:
        - name: varlog
          hostPath:
            path: /var/log
        - name: varlibdockercontainers
          hostPath:
            path: /var/lib/docker/containers
        - name: fluentd-config
          configMap:
            name: fluentd-config
            items:
              - key: fluent.conf
                path: fluent.conf

The FLUENT_ELASTICSEARCH_PASSWORD is the same password we defined on Elasticsearch configuration (ELASTIC_PASSWORD).

Define the environment variables in the ConfigMap.yaml file, we also define some custom Fluentd configuration. This configuration will be mounted on the Fluentd container. 

Deploy all your yaml files and make sure that Fluentd is running without any problems. If Fluentd is not running properly, you can tail the container logs or describe the Pod/DaemonSet. 

5. Deploying Kibana Deployment

Now we have Storage (Elasticsearch) and Data stream (Fluentd). Next, we need Kibana to view / edit / … the data. 

The configuration of Kibana is almost the same as Elasticsearch and Fluentd. We define some environment variables to create our Kibana configuration.

Mainly these configurations are:

  • username and password of the kibana user,
  • connection settings to Elastic search,
  • same Elasticsearch Certificate are mounted

5.1 Creating Kibana credentials

Kibana Username and Password

When we launch Kibana, the first question Kibana asks us is: "What is the username and password of the Kibana user". The username is kibana_system. The password can be retrieved from within the Elasticsearch container. 

Exec to one of the Elasticsearch containers and run the following command:

kubectl -n elasticsearch exec -ti elasticsearch-0 -- bash
 
#This will generate a random string. Save the password!
./bin/elasticsearch-reset-password -u kibana_system

Create a and deploy SealedSecret with the given password:

kubectl -n kibana create secret generic kibana-credentials \
  --from-literal=ELASTICSEARCH_PASSWORD='XXXXX' \
  --dry-run=client -o yaml | ${KUBESEAL_BINARY} --cert ${KUBESEAL_CERT_PATH} --format yaml > SealedSecret-KibanaCredentials.yaml

If you don't have SealedSecret controller, you can make a Secret resource by running the following command.

kubectl -n kibana create secret generic kibana-credentials \
  --from-literal=ELASTICSEARCH_PASSWORD='XXXXX' \
  --dry-run=client -o yaml > SealedSecret-KibanaCredentials.yaml

The command above will create the yaml file that needs to be deployed to the cluster. 

5.2 Creating Service

Now it is time to create the Service resource. Kibana will be accessible on port 5601 and use the app: kibana label to select the Service’s target Pods.

apiVersion: v1
kind: Service
metadata:
  name: kibana
  namespace: kibana
  labels:
    app: kibana
spec:
  selector:
    app: kibana
  ports:
    - name: http
      port: 80
      targetPort: 5601

5.3 Creating Deployment

Make sure that the ELASTICSEARCH_PASSWORD is defined in the environment variable and reads the right Secret.

We also need to create a new secret of the same PEM certificate we generated on the first step to mount on the container later.

# Create SealedSecret for the P12 file
kubectl -n kibana create secret generic elastic-certificate-pem \
  --from-file=elastic-certificates.p12 \
  --dry-run=client -o yaml | ${KUBESEAL_BINARY} --cert ${KUBESEAL_CERT_PATH} --format yaml > SealedSecret-KibanaCertificates.yaml

If you don't have SealedSecret controller, you can make a Secret resource by running the following command.

# Create SealedSecret for the P12 file
kubectl -n kibana create secret generic elastic-certificate-pem \
  --from-file=elastic-certificates.p12 \
  --dry-run=client -o yaml > SealedSecret-KibanaCertificates.yaml

The command above will create the yaml file that needs to be deployed to the cluster. 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kibana
  namespace: kibana
  labels:
    app: kibana
spec:
  replicas: 1
  selector:
    matchLabels:
      app: kibana
  template:
    metadata:
      labels:
        app: kibana
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - kibana
              topologyKey: kubernetes.io/hostname
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values:
                        - kibana
                topologyKey: topology.kubernetes.io/zone
      containers:
        - name: kibana
          image: docker.elastic.co/kibana/kibana:8.3.2
          resources:
             limits:
              cpu: 1000m
            requests:
              cpu: 100m
          env:
            - name: ELASTICSEARCH_HOSTS
              valueFrom:
                configMapKeyRef:
                  name: kibana-config
                  key: ELASTICSEARCH_HOSTS
            - name: SERVER_NAME
              valueFrom:
                configMapKeyRef:
                  name: kibana-config
                  key: SERVER_NAME
            - name: ELASTICSEARCH_USERNAME
              valueFrom:
                configMapKeyRef:
                  name: kibana-config
                  key: ELASTICSEARCH_USERNAME
            - name: ELASTICSEARCH_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: kibana-credentials
                  key: ELASTICSEARCH_PASSWORD
          ports:
            - containerPort: 5601
              name: http
              protocol: TCP
          volumeMounts:
            - name: kibana-certificates
              mountPath: /usr/share/kibana/config/certs
            - name: kibana-yml
              mountPath: /usr/share/kibana/config/kibana.yml
              subPath: kibana.yml
      volumes:
        - name: kibana-certificates
          secret:
            secretName: kibana-certificates
        - name: kibana-yml
          configMap:
            name: kibana-config
            items:
              - key: kibana.yml
                path: kibana.yml

5.4 Creating ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: kibana-config
  namespace: kibana
data:
  kibana.yml: |
    #
    # ** THIS IS AN AUTO-GENERATED FILE **
    #
     
    # Default Kibana configuration for docker target
    server.host: "0.0.0.0"
    server.shutdownTimeout: "5s"
    elasticsearch.hosts: [ "http://elasticsearch:9200" ]
    monitoring.ui.container.elasticsearch.enabled: true
    xpack.encryptedSavedObjects.encryptionKey: f9cd92d3834129433fb0404740b5e89c
    xpack.reporting.encryptionKey: e3de4fcf3fb5e6f973ce024121ead576
    xpack.security.encryptionKey: 4afebd157537e0f1b2c0b8deddff6b68
  SERVER_NAME: "kibana.example.com"
  ELASTICSEARCH_HOSTS: "http://elasticsearch.elasticsearch.svc.cluster.local:9200"
  ELASTICSEARCH_USERNAME: "kibana_system"

Deploy all configurations in the repository and tail the logs of Kibana.

And the Setup is Done!

You can now add your indexes, configure users, configure roles, ...  and monitor your logs!

6. Conclusion

In this Kubernetes tutorial we’ve demonstrated how to set up and configure Elasticsearch, Fluentd, and Kibana (EFK Stack) on a Kubernetes cluster.

Centralize and make the life of the developers easy by exposing container logs in one centralized logging infrastructure!

If you enjoyed this tutorial and want more info about these topics in the future, make sure to follow us on LinkedIn!