Collecte des logs dans Istio


 

Les logs nous permettent de savoir ce qui se passe réellement dans les systèmes afin de pouvoir détecter et gérer des incidents. Il est ainsi important, pour toute architecture de microservices en production, de pouvoir collecter des logs retraçant l’ensemble du trafic inter-microservices. Vu sous un certain angle, ceci peut s’avérer être compliqué dans un réseau de microservices du fait des multiples composants qui interagissent ensemble, mais avec Istio c’est assez simple. 

En effet avec une seule ligne de commande, on peut activer la génération des logs au niveau de chaque proxy Envoy.

Principe

Pour rappel, pour chaque microservice, Istio injecte un sidecar proxy Envoy qui se charge du routage de l’ensemble du trafic entrant et sortant.

Et comme dit précédemment, avec une seule ligne de commande,  on peut demander à Istio de configurer ces proxies Envoy de sorte que chacun écrive ses logs au niveau de son flux de sortie standard.

Dès lors, on pourra visualiser ces logs directement à partir de Kubernetes avec la commande kubectl logs ou à partir des outils de loggings comme Fluentd, Stackdriver Logging de Google Cloud ou la stack ELK (Elasticsearch, Logstash et Kibana).

Activation des logs

Pour activer la collecte des logs d’accès des proxies Envoy, il suffit de renseigner la variable values.global.proxy.accessLogFile au niveau de la configuration de Istio. Ceci peut se faire au moment de l’installation de Istio ou avec la commande suivante.

istioctl manifest apply --set values.global.proxy.accessLogFile="/dev/stdout"

Avec cette commande, on demande à Istio de configurer les proxies Envoy de sorte qu’ils impriment les logs d’accès sur leur sortie standard: /dev/stdout.

Par défaut, c’est avec le format suivant que les logs seront générés.

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

Pour personnaliser le format des logs d’accès des proxies Envoy, ISTIO met à notre disposition les paramètres suivants :

 

  • global.proxy.accessLogEncoding: Pour configurer l’encodage des logs en JSON ou en TEXT. La valeur par défaut est : TEXT
  • global.proxy.accessLogFormat: Pour configurer l’organisation et la mise en forme des champs des logs d’accès.

 

Pour plus de détails sur le formatage des logs des proxies Envoy, veuillez suivre ce lien: https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log#format-rulesPour désactiver la génération des logs, il suffit de donner une chaîne vide comme valeur de la variable values.global.proxy.accessLogFile.

istioctl manifest apply --set values.global.proxy.accessLogFile=""

Affichage des logs à partir de K8S

Comme dit précédemment, une fois qu’on configure les microservice de sorte qu’on ait les logs d’accès au niveau des sortie standard, on peut facilement récupérer ces derniers avec la commande kubectl logs.Par exemple, pour un microservice httpbin, voici la commande qui permet de visualiser tous les logs du/des proxy(s) Envoy associé(s) (Un microservice peut avoir plusieurs instances).

kubectl logs -l app=httpbin -c istio-proxy
  • L’option -l app=httpbin permet de sélectionner uniquement les pods qui ont des labels app=httpbin dans le namespace.
  • Le -c istio-proxy spécifie le conteneur à utiliser à l’intérieur des pods. Là il s’agit du sidecar proxy Envoy appelé Istio-proxy.

Voici un exemple de sortie de cette commande.

[2020-03-20T12:39:43.014Z] "GET /status/418 HTTP/1.1" 418 - "-" "-" 0 135 2 2 "-" "curl/7.64.0" "c1bf89bf-cc0e-4618-b839-119ce199f628" "httpbin:8000" "127.0.0.1:80" inbound|8000|http|httpbin.default.svc.cluster.local 127.0.0.1:42178 10.44.0.19:80 10.44.0.18:45620 outbound_.8000_._.httpbin.default.svc.cluster.local default

Pour comprendre la signification de chaque champ de cette sortie de log, on se base sur le format utilisé. Là il s’agit du format par défaut:

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

On peut lire donc qu’à la date du 2020-03-20T12:39:43.014Z, les logs du proxy envoy du microservice httpbin ont enregistré une requête HTTP entrante émanant du client 10.44.0.18:45620. On voit aussi  que cette requête est de type GET, l’url est le /status/418, le protocole utilisé est le HTTP version 1.1, le code de retour est le 418 et bien d’autres informations. 

Collecte des logs

Maintenant que la génération des logs des proxies Envoy est activée et qu’on arrive à les visualiser avec la commande kubectl logs, on peut passer à l’étape suivante et installer des outils adéquats pour la collecte, le stockage, l’analyse et la visualisation de nos logs.

Installation de Fluentd, Elasticsearch et Kibana

 /!\ le fonctionnement et la configuration détaillée de ces outils ne rentre pas dans le cadre de cet article. Pour plus de détails sur ces outils, je vous invite à aller visiter les documentations officielles.

Le principe est simple! On doit configurer fluentd de sorte qu’il se charge de la collecte des logs de chaque conteneur et qu’il les envoie à elasticsearch pour un stockage permanent. Puis configurer Kibana de sorte qu’il aille chercher les logs depuis elasticsearch pour la visualisation.

Pour ce faire, voici les manifestes qui permettent de créer les ressources nécessaires.

elasticsearch.yml

On commence par déployer un cluster de stockage Elasticsearch sur le namespace logging.

# Logging Namespace. All below are a part of this namespace.
apiVersion: v1
kind: Namespace
metadata:
 name: logging
---
# Elasticsearch Service
apiVersion: v1
kind: Service
metadata:
 name: elasticsearch
 namespace: logging
 labels:
   app: elasticsearch
spec:
 ports:
 - port: 9200
   protocol: TCP
   targetPort: db
 selector:
   app: elasticsearch
---
# Elasticsearch Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
 name: elasticsearch
 namespace: logging
 labels:
   app: elasticsearch
spec:
 replicas: 1
 selector:
   matchLabels:
     app: elasticsearch
 template:
   metadata:
     labels:
       app: elasticsearch
     annotations:
       sidecar.istio.io/inject: "false"
   spec:
     containers:
     - image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.1.1
       name: elasticsearch
       resources:
         # need more cpu upon initialization, therefore burstable class
         limits:
           cpu: 1000m
         requests:
           cpu: 100m
       env:
         - name: discovery.type
           value: single-node
       ports:
       - containerPort: 9200
         name: db
         protocol: TCP
       - containerPort: 9300
         name: transport
         protocol: TCP
       volumeMounts:
       - name: elasticsearch
         mountPath: /data
     volumes:
     - name: elasticsearch
       emptyDir: {}

Kibana.yml

Par la suite nous allons déployer Kibana en précisant en variable d’environnement l’emplacement du cluster elasticsearch depuis lequel il va récupérer les logs.

Kibana sera exposé en public via l’ingress gateway de ISTIO.

# Kibana Service
apiVersion: v1
kind: Service
metadata:
 name: kibana
 namespace: logging
 labels:
   app: kibana
spec:
 ports:
 - port: 5601
   protocol: TCP
   targetPort: ui
 selector:
   app: kibana
---
# Kibana Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
 name: kibana
 namespace: logging
 labels:
   app: kibana
spec:
 replicas: 1
 selector:
   matchLabels:
     app: kibana
 template:
   metadata:
     labels:
       app: kibana
     annotations:
       sidecar.istio.io/inject: "false"
   spec:
     containers:
     - name: kibana
       image: docker.elastic.co/kibana/kibana-oss:6.1.1
       resources:
         # need more cpu upon initialization, therefore burstable class
         limits:
           cpu: 1000m
         requests:
           cpu: 100m
       env:
         - name: ELASTICSEARCH_URL
           value: http://elasticsearch:9200
       ports:
       - containerPort: 5601
         name: ui
         protocol: TCP
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
 name: kibana-gateway
 namespace: logging
spec:
 selector:
   istio: ingressgateway
 servers:
 - port:
     number: 15033
     name: http-kibana
     protocol: HTTP
   hosts:
   - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
 name: kibana-vs
 namespace: logging
spec:
 hosts:
 - "*"
 gateways:
 - kibana-gateway
 http:
 - match:
   - port: 15033
   route:
   - destination:
       host: kibana
       port:
         number: 5601

fluentd.yaml

Pour le déploiement de fluentd, nous devons tout d’abord déployer un serviceaccount et le lier à un clusterrole afin d’autoriser le pod fluentd à utiliser l’API kubernetes.

Ensuite on déploie un DaemonSet Fluentd avec les bonnes variables d’environnement:

  • FLUENTD_UID : Mis à la valeur 0 afin de fournir au conteneur fluentd la permission de lire les logs se trouvant sur /var/log
  • FLUENTD_ELASTICSEARCH_X : Pour configurer la connection entre fluentd et le cluster elasticsearch.
apiVersion: v1
kind: ServiceAccount
metadata:
 name: fluentd
 namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
 name: fluentd
 namespace: kube-system
rules:
- apiGroups:
 - ""
 resources:
 - pods
 - namespaces
 verbs:
 - get
 - list
 - watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
 name: fluentd
roleRef:
 kind: ClusterRole
 name: fluentd
 apiGroup: rbac.authorization.k8s.io
subjects:
- kind: ServiceAccount
 name: fluentd
 namespace: kube-system
---
# Fluentd Service
apiVersion: v1
kind: Service
metadata:
 name: fluentd-es
 namespace: kube-system
 labels:
   app: fluentd-es
spec:
 ports:
 - name: fluentd-tcp
   port: 24224
   protocol: TCP
   targetPort: 24224
 - name: fluentd-udp
   port: 24224
   protocol: UDP
   targetPort: 24224
 selector:
   k8s-app: fluentd-logging
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
 name: fluentd
 namespace: kube-system
 labels:
   k8s-app: fluentd-logging
   version: v1
   kubernetes.io/cluster-service: "true"
spec:
 template:
   metadata:
     labels:
       k8s-app: fluentd-logging
       version: v1
       kubernetes.io/cluster-service: "true"
   spec:
     serviceAccount: fluentd
     serviceAccountName: fluentd
     tolerations:
     - key: node-role.kubernetes.io/master
       effect: NoSchedule
     containers:
     - name: fluentd
       image: fluent/fluentd-kubernetes-daemonset:v1.3-debian-elasticsearch
       env:
         - name:  FLUENT_ELASTICSEARCH_HOST
           value: "elasticsearch.logging"
         - name:  FLUENT_ELASTICSEARCH_PORT
           value: "9200"
         - name: FLUENT_ELASTICSEARCH_SCHEME
           value: "http"
         - name: FLUENT_UID
           value: "0"
       resources:
         limits:
           memory: 200Mi
         requests:
           cpu: 100m
           memory: 200Mi
       volumeMounts:
       - name: varlog
         mountPath: /var/log
       - name: varlibdockercontainers
         mountPath: /var/lib/docker/containers
         readOnly: true
     terminationGracePeriodSeconds: 30
     volumes:
     - name: varlog
       hostPath:
         path: /var/log
     - name: varlibdockercontainers
       hostPath:
         path: /var/lib/docker/containers

Visualisation des logs depuis Kibana

On peut accéder à l’UI de Kibana avec l’IP publique d’un ingress gateway qu’on aura créé, ou faire une redirection de port avec la commande suivante et y accéder à partir de http://localhost:5601/.

kubectl -n logging port-forward $(kubectl -n logging get pod -l app=kibana -o jsonpath='{.items[0].metadata.name}') 5601:5601 &

Une fois sur l’écran d’accueil, dans l’onglet « Management », on crée un index pattern avec logstash* ou * comme nom d’index.

Après ça, en allant au niveau de Discovery, on pourra visualiser les logs de nos proxies Envoy.

Liens utiles:

https://istio.io/docs/concepts/observability/

https://istio.io/docs/tasks/observability/logs/access-log/

https://istio.io/docs/tasks/observability/logs/collecting-logs/

Conclusion:

Sur ISTIO, on a aucun système de logging par défaut. Il faut donc l’activer au besoin. Et plutôt que de visualiser les logs sur la sortie standard stdout, l’idéal est d’intégrer une stack de logging comme on vient de le voir avec EFK. Celle ci propose une solution de logging assez puissante qui permet de collecter, stocker, analyser et visualiser nos logs.

Et bien que le logging soit essentiels pour diagnostiquer les incidents sur un services mesh, le tracing peut offrir beaucoup plus de détails sur notre mesh et des incidents qui peuvent survenir dessus. On verra dans un prochain article comment cette solution de tracing se présente, et comment ou pourra l’implémenter dans un service mesh.