Share:

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 :

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 :

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 :

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 :

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 :

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.