
Les microservices, les containers, le serverless… ces tendances architecturales actuelles ont en commun de favoriser une granularité applicative plus fine afin de faciliter l’organisation des développements, augmenter la cadence des déploiements, sécuriser la montée en charge et fiabiliser la maintenance sur le long terme. Mais elles ont aussi des contreparties, et la complexité du monitoring en est une. Plus un système est distribué, décomposé en sous-systèmes indépendants qui communiquent entre eux par des flux de plus en plus nombreux, plus sa surveillance et la compréhension des incidents deviennent une tâche ardue. De cette complexité est née l’observabilité. Au-delà du monitoring, l’observabilité permet la détection et l’analyse d’un incident sur un système qui génère une quantité très importantes d’indicateurs qu’on ne sait pas forcément appréhender a priori.
Une plateforme d’observabilité repose sur trois piliers :
- Les métriques, qu’elles soient de niveau système ou métier, permettent de répondre à la question “est-ce que j’ai un problème ?”
- Les traces permettent de répondre à la question “d’où vient le problème ?” en identifiant le composant qui en est à l’origine,
- Les logs permettent de répondre à la question “pourquoi le problème se produit ?” en identifiant sa root cause.
La qualité d’une solution d’observabilité se mesure notamment par sa capacité à ingérer rapidement le volume important de métriques produit par le système qu’elle supervise, afin de minimiser la durée de détection et de réaction à un incident.
Le présent article a pour but de vous présenter un exemple d’architecture simple mais complet qui intègre :
- Une infrastructure réseau typique sur le cloud AWS,
- Une application containerisée avec Docker qui expose une API sur Internet,
- Un cluster Kubernetes implémenté au travers du service managé EKS (Elastic Kubernetes Service) pour l’orchestration des containers,
- L’intégration du service Splunk Infrastructure Monitoring pour l’observabilité.
L’objectif est également de démontrer la pertinence de Terraform pour le développement de l’infrastructure et le déploiement de toutes les composantes de cette architecture.
L’intégralité du code source est publiée sur GitHub et une présentation vidéo est également disponible sur la chaîne Youtube de Cloudreach.
Infrastructure réseau AWS
La première étape dans l’utilisation du cloud AWS consiste à choisir au moins une région dans laquelle déployer ses ressources. Dans cet exemple nous choisissons “eu-west-1” qui correspond à l’Irlande. Ensuite, la mise en place d’une infrastructure réseau est un pré-requis au déploiement d’autres ressources. Le VPC (Virtual Private Cloud) est l’élément de base qui permet de créer un réseau privé et isolé dans le cloud AWS. On lui attache une plage d’adresses IP appelée CIDR (Classless Inter-Domain Routing) block range. Ce VPC doit être découpé en subnets. À chacun d’eux est attribuée une partie du CIDR block range ainsi qu’une zone de disponibilité. Les zones de disponibilité sont des sous-parties d’une région qui sont isolées les unes des autres de telle sorte que l’indisponibilité de l’une d’entre elles n’impacte pas les autres. Il est donc important de répartir ses ressources sur plusieurs zones de disponibilité afin de maximiser la disponibilité de son service. On distingue deux types de subnet :
- Les subnets publics sont accessibles depuis Internet car les ressources qui y sont attachées peuvent disposer d’une adresse IP publique. La table de routage de ces subnets contient une route ayant pour destination 0.0.0.0/0 (soit any) et pour cible une Internet Gateway qui est l’élément rattaché au VPC permettant de communiquer avec Internet.
- Les subnets privés ne sont pas accessibles depuis Internet car les ressources qui y sont attachées disposent uniquement d’adresses IP privées. En revanche, il est possible d’accéder à Internet depuis ces subnets par l’intermédiaire d’une route ayant pour destination 0.0.0.0/0 (soit any) et pour cible une NAT Gateway déployée dans un subnet public et dont le rôle est de faire la translation entre les adresses IP privées et les adresses IP publiques.
L’utilisation d’un langage permettant de coder son infrastructure — ou faire de l’IaC pour Infrastructure as Code — est une bonne pratique afin de favoriser l’industrialisation, la cohérence et la réutilisabilité.
Terraform est un outil d’IaC particulièrement adapté dans notre cas du fait qu’il permette, au sein d’un même code source et d’un même déploiement, de provisionner des ressources dans plusieurs systèmes cibles.
AWS est le premier système dans lequel nous allons créer les ressources réseau précédemment décrites. Pour ce faire, il est nécessaire de définir un provider de la façon suivante.
provider aws { region = var.region}
Davantage de paramètres peuvent être précisés mais nous nous contenterons de fournir la région cible. Celle-ci est définie dans une variable qui dispose d’une valeur par défaut.
variable region { description = "AWS region to deploy to" type = string default = "eu-west-1"}
Terraform dispose d’une registry publique de modules qui ont pour rôle d’abstraire la complexité de création d’un ensemble de ressources. C’est ce que nous utilisons pour le déploiement de l’infrastructure réseau AWS précédemment décrite. Nous ne précisons que les paramètres nécessaires en entrée du module VPC.
module vpc { source = "terraform-aws-modules/vpc/aws" version = "2.66.0" name = var.vpc_name cidr = var.vpc_cidr azs = data.aws_availability_zones.available.names private_subnets = var.private_subnets public_subnets = var.public_subnets enable_nat_gateway = true public_subnet_tags = { "kubernetes.io/cluster/${var.eks_cluster_name}" = "shared" "kubernetes.io/role/elb" = "1" } private_subnet_tags = { "kubernetes.io/cluster/${var.eks_cluster_name}" = "shared" "kubernetes.io/role/internal-elb" = "1" }}
De nouvelles variables ont été utilisées, ainsi qu’une data source qui nous permet de récupérer dynamiquement la liste des zones de disponibilité correspondantes à la région que nous avons choisie.
variable vpc_name { description = "Name of the VPC" type = string default = "eks-vpc"}variable vpc_cidr { description = "CIDR of the VPC" type = string default = "10.0.0.0/16"}variable private_subnets { description = "Private subnets CIDR list" type = list(string) default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]}variable public_subnets { description = "Public subnets CIDR list" type = list(string) default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]} data aws_availability_zones available { state = "available"}
Les tags positionnés sur les subnets du VPC permettent de préciser au moteur Kubernetes qu’il pourra les utiliser pour le déploiement de services et de quelle façon le faire. En particulier, les load balancers internes pourront être créés dans les subnets privés et les load balancers exposés à Internet pourront être créés dans les subnets publics.
Cluster Kubernetes
AWS propose le service managé EKS (Elastic Kubernetes Service) pour le déploiement d’un cluster Kubernetes et c’est la solution qui est à privilégier pour s’économiser l’effort de maintenance qui n’est pas négligeable. Le cluster est créé dans notre VPC et les worker nodes se matérialisent par des instances EC2 (Elastic Cloud Compute, machines virtuelles dans le cloud AWS) déployées dans les subnets privés.
Ici encore, un module Terraform est disponible dans la registry publique et nous avons tout intérêt à l’utiliser pour simplifier notre code source.
module eks { source = "terraform-aws-modules/eks/aws" version = "13.2.1" cluster_name = var.eks_cluster_name cluster_version = var.eks_cluster_version cluster_enabled_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"] subnets = module.vpc.private_subnets vpc_id = module.vpc.vpc_id map_users = [ for user in data.aws_iam_user.eks_master_users : { userarn = user.arn username = user.user_name groups = ["system:masters"] } ] node_groups = { group1 = { name = "workers-group1" instance_type = var.eks_node_group_instance_type desired_capacity = var.eks_node_group_desired_capacity } }}
De nouvelles variables ont été déclarées pour l’instanciation du service EKS.
variable eks_cluster_name { description = "EKS cluster name" type = string default = "eks-cluster"}variable eks_cluster_version { description = "EKS cluster version" type = string default = "1.18"}variable eks_node_group_instance_type { description = "EKS node group instances type" type = string default = "t3.small"}variable eks_node_group_desired_capacity { description = "EKS node group desired capacity" type = number default = 3}variable eks_master_usernames { description = "IAM usernames list to add as system:masters to the aws-auth configmap" type = list(string) default = []}
Le module que nous utilisons nécessite l’ajout d’un provider afin de paramétrer l’authentification au sein du cluster Kubernetes.
provider kubernetes { host = data.aws_eks_cluster.cluster.endpoint cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) token = data.aws_eks_cluster_auth.cluster.token}
Ce provider interagit avec le cluster Kubernetes, qui une ressource créée au sein du même code source via le provider AWS. Ces attributs doivent donc être valorisés dynamiquement à l’aide de data sources.
data aws_eks_cluster cluster { name = module.eks.cluster_id}data aws_eks_cluster_auth cluster { name = module.eks.cluster_id}
Workload applicative
Une workload est déployée à titre d’exemple. Il s’agit d’une simple API HTTP REST exposée sur Internet au travers d’une image Docker nommée server déployée sur trois pods, et d’un Load Balancer en zone publique. Une image client est également déployée dans le but d’interroger l’API en continu afin de générer de l’activité sur le cluster Kubernetes. Nous verrons à la fin de l’article que cette application présente une anomalie de développement qui n’est visible qu’après exécution avec une charge significative.
Le code source applicatif est disponible dans le répertoire /k8s_splunk/appli du repository GitHub. Il contient pour le client et le server : le code source en Python, le Dockerfile pour construire l’image Docker à l’aide du script build.sh, ainsi qu’un script push.sh permettant de publier l’image vers un repository ECR (Elastic Container Registry) qui est le service de registry Docker sur AWS. Cette étape est nécessaire en amont du déploiement de l’application sur le cluster EKS.
Le provider Kubernetes précédemment créé est utilisé une nouvelle fois pour encapsuler ce déploiement avec Terraform. Un namespace app, deux deployments client et server, et un service server sont ajoutés à notre code source afin de créer les pods et le load balancer externe. Ils sont détaillés dans le fichier /k8s_splunk/infra/terraform/services.tf. La syntaxe de ces ressources est très proche de celle des fichiers YAML traditionnellement utilisés avec l’outil kubectl. Ici Terraform est utilisé comme une surcouche afin d’avoir un seul et unique outil de déploiement, et pour alimenter dynamiquement le cluster Kubernetes qui est créé au sein du même code source.
Splunk Infrastructure Monitoring
Splunk Infrastructure Monitoring est un des composants de la plateforme d’observabilité proposée par Splunk suite au rachat de SignalFX en 2019. Il fonctionne en mode SaaS et il est possible de tester gratuitement le service pour une durée de 14 jours en remplissant le formulaire Free Trial disponible à cette adresse. C’est ce que nous utilisons ici pour illustrer l’intégration de Kubernetes à une solution d’observabilité.
L’intégration à Splunk Infrastructure Monitoring nécessite le déploiement d’un agent sur le cluster Kubernetes qui a pour rôle de collecter les métriques sur chacun des worker nodes et de les envoyer par Internet vers le service Splunk. Cet agent est disponible publiquement sous la forme d’un chart Helm. Helm est un gestionnaire de paquets pour Kubernetes qui a pour objectif de faciliter le packaging et la distribution d’applications en masquant la complexité des éléments qui la composent. Un provider Terraform existe pour Helm et nous permettra, une fois de plus, d’encapsuler l’utilisation de cet outil afin de conserver une unique solution de déploiement.
provider helm { kubernetes { host = data.aws_eks_cluster.cluster.endpoint cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data) token = data.aws_eks_cluster_auth.cluster.token }}
Les paramètres du provider Helm sont identiques à ceux du provider Kubernetes.
resource helm_release signalfx_agent { name = "signalfx-agent" namespace = "splunk" create_namespace = true repository = "https://dl.signalfx.com/helm-repo" chart = "signalfx-agent" set { name = "clusterName" value = var.eks_cluster_name } set { name = "signalFxAccessToken" value = var.signalfx_access_token } set { name = "signalFxRealm" value = var.signalfx_realm } values = [file("splunk-values.yaml")]}
Une seule ressource est ajoutée : une release Helm, qui consiste à déployer le chart signalfx-agent disponible sur le repository public https://dl.signalfx.com/helm-repo. Ce chart requiert trois paramètres obligatoires :
- clusterName est le nom du cluster qui sera affiché sur l’interface Splunk. Le choix de cette valeur est libre et nous choisissons de positionner la même valeur que lors de la création du cluster sur AWS.
- signalFxAccessToken est le secret permettant à l’agent de s’authentifier lors de la connexion au service Splunk. Dans l’interface Splunk, la rubrique Access Tokens de la section My Profile permet de gérer ces tokens et de récupérer la valeur qui doit être positionnée dans ce paramètre.
- signalFxReal représente le domaine sur lequel votre environnement Splunk a été créé. Sa valeur est directement disponible dans la section My Profile de l’interface Splunk.
Des variables sont ajoutées pour ces deux derniers paramètres.
variable signalfx_access_token { description = "Token used to authenticate your connection to SignalFx" type = string}variable signalfx_realm { description = "Name of the realm in which your organization is hosted, as shown on your profile page in the SignalFx web application" type = string}
Il est possible de configurer plus finement le comportement de l’agent au travers d’un fichier de configuration YAML disponible sur le repository GitHub de Splunk à cette adresse. Cette étape n’est pas obligatoire car la configuration par défaut fonctionne très bien dans la plupart des cas, mais nous la modifions ici pour deux raisons :
- Le paramètre metricIntervalSeconds définit la fréquence d’envoi des métriques et sa valeur par défaut est de 10 secondes. Nous le diminuons à 1 seconde afin d’optimiser la réactivité de notre solution d’observabilité.
- La collecte des métriques de l’API kubelet se connecte par défaut au port TCP 10255 des worker nodes. Ce port étant par défaut fermé sur les instances EKS, nous le remplaçons par le port 10250. Il s’agit d’une spécificité de l’implémentation AWS des worker nodes Kubernetes. Cette modification nécessite la redéfinition du monitor kubelet-metrics au sein du paramètre monitors.
monitors: - type: kubelet-metrics kubeletAPI: url: https://localhost:10250 authType: serviceAccount usePodsEndpoint: true
Ce fichier de configuration modifié est fourni en entrée de la release Helm au travers du paramètre values précédemment valorisé.
Déploiement
La procédure de déploiement est décrite en détail dans le fichier README du repository GitHub.
Une fois déployé, il est à noter que le module EKS qui est utilisé génère un fichier kubeconfig_<eks_cluster_name> contenant la configuration de l’outil kubectl pour se connecter au cluster Kubernetes. Il suffit pour cela de valoriser la variable d’environnement KUBECONFIG avec le chemin et le nom de ce fichier.
Résultats et conclusion
Les représentations graphiques des métriques collectées sont disponibles dans l’interface de Splunk Infrastructure Monitoring quelques secondes seulement après la fin du déploiement.
La vue Cluster Map de Kubernetes Navigator offre une représentation graphique de notre cluster pour une vision globale de la santé du système avec la possibilité de faire du drill down afin d’ajuster la granularité de l’analyse.
Davantage de vues sont disponibles pour analyser le comportement par node, workload, pod ou container avec toujours la possibilité de filtrer les résultats en temps réel. Cela nous permet, dans notre exemple, d’identifier une fuite mémoire sur notre container server au regard de la courbe de sa consommation mémoire qui est en constante augmentation comme on peut le voir dans la dernière capture. Les propriétés du déploiement sont également visibles dans l’interface et nous montrent que la Memory Limit de notre container n’a pas été définie, ce qui n’est pas conforme aux bonnes pratiques.
Il est possible de créer des alertes de différentes natures, sur mesure ou à partir de templates, pour détecter par exemple la variation d’une métrique par rapport à sa norme historique ou sa population normale. Compte tenu de la rapidité de collecte et de la granularité très fine des métriques que nous avons réglée à une seconde, nous obtenons ici une solution d’observabilité qui nous permettra d’avoir une réactivité optimale en cas d’incident sur notre système Kubernetes.
This post was originally available on Medium on 25/01/2021.