Published by

Il y a 1 mois -

Temps de lecture 20 minutes

Terraform sur Google Cloud Platform

On ne remet plus en question aujourd’hui la nécessité de déployer son infrastructure de manière automatisée.

Sur Google Cloud Platform plusieurs solutions sont possibles :

  • Le client en ligne de commande gcloud, mais déployer toute son infrastructure en shell n’est clairement pas une solution
  • Deployment Manager, le service fourni sur GCP pour faire de l’infra as code
  • Terraform, qui est une référence aujourd’hui en terme de déploiement de ressource cloud, mais est-ce un choix judicieux ?
  • D’autres outils comme Pulumi, qui sont des choix intéressants mais pas abordés dans cet article

 

Vous avez ouvert cet article, vous savez donc que l’objectif est de parler de Terraform, je vous propose donc ce petit guide pour bien démarrer :

Les exemples qui vont illustrer les différents chapitres sont disponibles sur https://github.com/ibeauvais/terraform-getting-started

Terraform ou Deployment manager ?

Évidemment lorsqu’on souhaite avoir une approche infra as code sur Google Cloud Platform, la question suivante se pose :

Pourquoi ne pas utiliser l’outil natif fourni ? N’est il pas plus à jour et mieux intégré à l’écosystème GCP ?

Sans entrer dans les considérations de syntaxe ou de comparaison de fonctionnalité tous les deux ont leurs avantages et inconvénients. Terraform nécessitera des ressources déjà déployées avant de pouvoir commencer, notamment pour la gestion du state mais il est plus utilisé et mieux documenté. Comme c’est un projet open source et qu’il n’est pas utilisé que pour Google Cloud Platform, il bénéficie d’une communauté plus importante. Et dans le cas d’une architecture multi-cloud ou l’utilisation de solution gérée par un des nombreux providers terraform il sera donc possible de déployer avec le même outil.

Pour ce qui est des mises à jour, spécifiquement pour GCP, le provider est maintenu conjointement par Hashicorp et Google. Et grâce à la génération automatique du code directement à partir des API Google à l’aide de Magic Module, il est possible de déployer des services avec Terraform alors même qu’ils viennent de sortir en beta.

Terraform 101

Avec terraform, on décrit son infrastructure en HCL (HashiCorp Configuration Language) , un langage dédié conçu par Hashicorp, avec un système de variable, types, interpolations, boucles, etc…

Illustrons cela avec un exemple simple : je souhaite créer un bucket cloud storage :

resource "google_storage_bucket" "simple-bucket" {
  name = "simple-bucket-abc"
  location = "europe-west1"
}

Cela ne suffit pas puisque Terraform n’est pas fait uniquement pour déployer sur Google Cloud Platform : il est possible de déployer un grand nombre de ressources différentes grâce à son système de provider.

Dans notre cas je vais donc ajouter le provider Google Cloud platform :

provider "google" {
  project = "my-gcp-project-id"
}

A noter, qu’il faut spécifier un identifiant projet qui va être le projet gcp par défaut où sont déployées les ressources mais aussi dans lequel les appels d’API vont être fait.

Une fois le code écrit dans un fichier .tf, je récupère terraform, un simple exécutable écrit en go (donc, sans dépendance supplémentaire) permettant d’exécuter les commandes suivantes
:

  • terraform init pour initialiser l’espace de travail et télécharger la dernière version du provider.
  • terraform plan pour voir le plan d’exécution contenant toutes les actions nécessaires pour que l’état sur le projet GCP corresponde à ce qui se trouve dans les sources .tf.

Avant de lancer le plan je dois authentifier mes appels terraform avec mon compte sur google cloud et avoir les droits nécessaires pour créer un bucket sur ce projet :

gcloud auth application-default login

Résultat du plan :

Resource actions are indicated with the following symbols:
  + create
 
Terraform will perform the following actions:
 
  # google_storage_bucket.simple-bucket will be created
  + resource "google_storage_bucket" "simple-bucket" {
      + bucket_policy_only = (known after apply)
      + force_destroy      = false
      + id                 = (known after apply)
      + location           = "EUROPE-WEST1"
      + name               = "simple-bucket-abc"
      + project            = (known after apply)
      + self_link          = (known after apply)
      + storage_class      = "STANDARD"
      + url                = (known after apply)
    }
 
Plan: 1 to add, 0 to change, 0 to destroy.
  • terraform apply pour appliquer ce plan, terraform fera de nouveau un plan, et demandera confirmation avant d’appliquer la modification.

A chaque exécution Terraform va donc faire une différence entre ce qu’il a déjà déployé et ce qui se trouve dans les sources, puis :

  • Créer les ressources en plus.
  • Supprimer les ressources en moins.
  • Mettre à jour les ressources qui ont changé.

Pour être capable de faire cette différence, Terraform lors de la première exécution va générer un état le « state terraform » qui contient l’ensemble des ressources qui ont été créées. C’est un peu la base de données de terraform, sous forme d’un simple fichier json.

Si je veux voir ce state, j’ouvre le fichier terraform.tfstate qui se trouve, dans cet exemple, là où j’ai lancé la commande terraform:

{
 "version": 4,
 "terraform_version": "0.12.18",
 "serial": 7,
 "lineage": "dedc9f7b-14f8-25dc-9752-3a6306a69b19",
 "outputs": {},
 "resources": [
   {
     "mode": "managed",
     "type": "google_storage_bucket",
     "name": "simple-bucket",
     "provider": "provider.google",
     "instances": [
       {
         "schema_version": 0,
         "attributes": {
           "bucket_policy_only": false,
           "cors": [],
           "default_event_based_hold": false,
           "encryption": [],
           "force_destroy": false,
           "id": "simple-bucket-abc",
           "labels": null,
           "lifecycle_rule": [],
           "location": "EUROPE-WEST1",
           "logging": [],
           "name": "simple-bucket-abc",
           "project": "my-gcp-project-id",
           "requester_pays": false,
           "retention_policy": [],
           "self_link": "https://www.googleapis.com/storage/v1/b/simple-bucket-abc",
           "storage_class": "STANDARD",
           "url": "gs://simple-bucket-abc",
           "versioning": [],
           "website": []
         },
         "private": "bnVsbA=="
       }
     ]
   }
 ]
}

Configuration de l’authentification

Dans l’exemple précédent, un choix simple a été fait de lancer terraform avec son propre compte personnel en s’identifiant avec la commande :

gcloud auth application-default login

Ce n’est en fait pas la solution recommandée car certaines API ne sont pas compatibles avec ce mode d’authentification.

Sur Google Cloud Platform, la bonne manière de s’authentifier est d’utiliser un compte de service. Ce compte doit avoir les permissions et donc les rôles lui permettant de créer les ressources. Il est donc possible et conseillé de limiter ces permissions à ce qu’il doit avoir besoin de faire. Par exemple, un compte de service qui ne peut pas supprimer de projet GCP évitera certaines surprises en cas d’erreur dans le code.

Plusieurs moyens sont possibles pour utiliser ce compte de service :

  • Renseigner les variables d’environnement GOOGLE_CREDENTIALS, GOOGLE_CLOUD_KEYFILE_JSON ou GCLOUD_KEYFILE_JSON avec le chemin du compte de service
  • Si le terraform est exécuté sur une VM
    Compute Engine
    il utilisera directement le compte de service de la VM
  • Lui spécifier le path de la clé json du compte de service dans la configuration du provider :
provider "google" {
  credentials = file("sa/my-project-service-account.json")
}

Cette dernière solution est à  éviter, vous ne voulez pas avoir dans votre code un chemin en dur de clé sans compter le risque dans cet exemple d’envoyer sa clé dans son repository git.

Le problème avec cela, c’est que très souvent, on préférait faire un plan avant de pousser son code. Et pour cela, télécharger sur sa machine la clé du compte ce qui n’est pas souhaitable pour des raisons de sécurité.

Pour résoudre ce problème il est possible d’utiliser une autre manière de s’authentifier, le mécanisme d’impersonation et de création de token temporaire des comptes de service : 

En pré-requis : un compte de service qui porte les droits pour déployer les ressources que je vais appeler pour l’exemple terraform-deployer

  • On déclare un premier provider google :  ce provider sera authentifié avec le compte qui lance le terraform : soit l’utilisateur authentifié avec gcloud auth application-default login, soit le compte de service de la CI. Cependant, pour avoir le droit de faire cela il est nécéssaire d’avoir le rôle iam.serviceAccountTokenCreator sur le compte de service de déploiement terraform-deployer
provider "google" {
 project = var.project
 alias  = "token_gen"
 
 scopes = [
   "https://www.googleapis.com/auth/cloud-platform",
   "https://www.googleapis.com/auth/userinfo.email",
 ]
}
  • Ce premier provider va servir à récupérer un token d’authentification temporaire pour le compte de service terraform-deployer : 
data "google_service_account_access_token" "default" {
 provider               = google.token_gen
 target_service_account = "terraform-deployer@xxx-id-project.
 
 scopes = [
   "userinfo-email",
   "cloud-platform",
 ]
 
 lifetime = "300s"
}
  • On déclare ensuite un second provider qui va utiliser ce token pour authentifier les appels de terraform. Ce sera ce provider qui sera utilisé pour déployer les ressources avec le compte de service terraform-deployer :
provider "google" {
 project = "xxx-id-project"
 access_token = data.google_service_account_access_token.default.access_token
}

Plus de détail sur l’impersonation avec terraform ici :  How to generate and use temporary credentials on Google Cloud Platform

Gestion du state Terraform

Dans le premier exemple le ‘state terraform’
était en local, ce qui n’est pas une solution viable pour la gestion de mon infrastructure. Il est important d’avoir le state persisté et si également versionné.

Dans Terraform, il est possible de configurer le stockage du state de plusieurs manières, grace à la configuration de « backends ». Par exemple dans une base de donnée, via une api rest ..etc

Il existe justement un backend google cloud storage qui va permettre de stocker ce state sur un bucket à condition d’avoir les droits en écriture sur ce bucket.

Bien sur, ce bucket ne peut pas être créé par le code terraform qui l’utilise comme backend, il faut donc créer ce bucket avant : soit manuellement, soit avec un terraform de plus haut niveau, soit avec une autre solution d’infra as code, deployment manager par exemple sur google cloud platform.

Pour l’exemple partons sur la solution manuelle  :

#Bucket creation
 
gsutil mb -p xxx-my-project-id gs://terraform-state-project-xxx/

Une fois le bucket créé il me suffit de spécifier dans le code terraform que j’utilise un backend cloud storage:

terraform {
 
 backend "gcs" {
 
   bucket  = "terraform-state-xxxx-my-project-id"
 
   prefix  = "simple"
 
 }
 
}

A noter, le paramètre prefix pour écrire ce state dans un « sous répertoire ». En faisant un terraform init j’aurai alors dans ce bucket :

Tips : Versioning du state

Activez le versioning sur le bucket de state, cela vous permettra de revenir en arrière en cas d’erreur, mise à jour malheureuse de terraform ou corruption du fichier:

gsutil versioning set on gs://terraform-state-project-xxx/

Ensuite pour voir l’historique  : 

gsutil ls -a gs://terraform-state-xxxx-my-project-id/simple/default.tfstate
gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280095904936
gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280172775505
gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280187357402
gs://terraform-state-xxxx-my-project-id/simple/default.tfstate#1591280195113456

 

Tips: Configuration du backend

Il est possible de configurer le backend via des arguments passés à la commande terraform afin de rendre indépendant le code et l’emplacement de stockage du state :

terraform {
 backend "gcs" {
 }
}

Ensuite :

terraform init -backend-config=”bucket=terraform-state-xxxx-my-project-id" -backend-config=”prefix=simple"

ou  via un fichier de config externe :

terraform init -backend-config=/home/xxx/config/backend-config.tf

Ressources et Organisation

Nous avons vu que pour déployer avec terraform dans un projet Google Cloud Platform il est nécéssaire d’avoir 3 pré-requis :

  • Un compte de service
  • Des rôles associées à ce compte de service pour avoir le de droit de créer/modifier/supprimer les ressources
  • Un bucket pour le state

Et donc un projet GCP pour héberger cela.

3 solutions sont possibles :

1. Le compte de service et le bucket pour le state sont dans le projet où je souhaite déployer les ressources

Cela revient à dire que le terraform ne porte pas la création de notre projet mais va s’occuper uniquement d’ajouter des ressources dans celui ci. En effet, le projet et les pré-requis doivent être créés avant de pouvoir commencer à utiliser terraform. Cette solution est simple à mettre en place car ne nécessite que un seul projet.

2. Le compte de service et le bucket pour le state sont dans un autre projet

Cette solution permet un scope plus large d’automatisation puisque en fonction des droits accordés au compte de service, il sera possible de gérer tout le cycle de vie d’un projet, voir gérer plusieurs projets, par exemple les n environnements d’une même application.

Un exemple simple pourrait être ainsi :

Mais on peut aller bien plus loin en donnant des droits sur un folder pour pouvoir gérer tous les projets enfants :

Le projet A peut très bien être également dans le même folder.

3. Un mix entre les 2 solutions :

Rien n’oblige à gérer toute l’infrastructure d’un projet avec un seul terraform. Il est possible de gérer la création et les configurations de base d’un projet avec un terraform géré dans un projet externe et de déléguer la création des ressources simples à un autre terraform avec un scope plus réduit de permissions. Le premier terraform ajoutera le compte de service et le bucket pour le state pour le second. Cette solution est la plus complexe : elle offre une séparation des responsabilités plus fine, permet de gérer avec un cycle de vie différent les configurations qui ne changent pas souvent et qui nécessitent des droits importants comme les permissions IAM du projet ou l’activation des services et les configurations qui vont évoluer régulièrement comme les buckets, les VM ou les services managés. Par contre l’infrastructure du projet est séparée en 2 codes avec un lien fort entre les 2.

Dans l’exemple ci-dessus

  • Le terraform dans Project A va créer project B et project C dans le parent Folder attribuer les rôles et activer les services.
  • Le terraform dans Project B va créér les VM / Buckets / Clusters GKE dans le Project B
  • Le terraform dans Project C va créér les VM / Buckets / Clusters GKE dans le Project C

Cela veut donc dire que le terraform dans Project B aura parfois besoin de ressources créées dans le terraform du Project A, (et pas l’inverse il faut garder en tête que le projet A est le parent)

Gestion des projets et folders Google Cloud

Nous avons vu dans le chapitre précédent qu’il est possible de créer et gérer entièrement un projet avec terraform. Pour cela il existe la ressource terraform google_project, mais gérer via infra as code un projet implique un certain nombre de choses :

  • Le projet doit être rattaché à un parent, organisation ou folder.
  • Il doit être également rattaché à un compte de billing pour la facturation.
  • Il doit avoir un ID unique sur GCP, de maximum 30 caractères alphanumériques ou tiret.
  • Pour être opérationnel il doit avoir des API d’activées

Cela entraine donc un certain nombre de pré-requis en terme de rôle pour le compte de service qui va exécuter le terraform et en terme de service activé  :

  • Au minimum, le role roles/billing.user  sur le compte de billing à rattacher au projet
  • L’API cloudbilling.googleapis.com d’activé sur le projet que l’on utilise avec le provider
  • Au minimum, le role roles/resourcemanager.projectCreator  sur le parent dans lequel on va créer le projet (folder ou organisation)

A partir de là, créer un projet est très simple :

resource "google_project" "my_project_in_a_folder" {
  name            = "my_unique_project_id"
  project_id      = "my_unique_project_id"
  folder_id       = "1234567891011"
  billing_account = "0123456-ABCDEF-GHIJK"
}

Et activer les services que l’on souhaite utiliser sur  ce projet :

locals {
  services = ["storage-component.googleapis.com", "compute.googleapis.com"]
}
 
resource "google_project_service" "services" {
  for_each = toset(local.services)
  project  = google_project.my_project_in_a_folder.project_id
  service  = each.value
}

Tips: Activation des services

Dans l’exemple précédent, j’active les services Cloud Storage  (storage-component.googleapis.com) et Compute Engine (compute.googleapis.com)

La liste des services activables peut être trouvé dans  API & Services

Si j’affiche la liste des services activés après le apply du terraform :

gcloud services list

Le nombre de services activés du projet est plus important que dans le terraform : 

NAME TITLE
compute.googleapis.com Compute Engine API
oslogin.googleapis.com Cloud OS Login API
storage-component.googleapis.com Cloud Storage

En effet, activer certains services va entrainer automatiquement certaines actions, comme activer d’autres services nécessaires ou ajouter des comptes de service et des rôles pour des taches particulières du service.

Par exemple activer un service important comme Kubernetes Engine API (container.googleapis.com) va :

  • Activer les services BigQuery API, BigQuery Storage API, Compute Engine API, Container Registry API, Identity and Access Management (IAM) API, IAM Service Account Credentials API, Cloud Monitoring API, Cloud OS Login API, Cloud Pub/Sub API, Google Cloud Storage JSON API
  • Ajouter au projet le compte de service service-<project_number>@container-engine-robot.iam.gserviceaccount.com avec le rôle roles/container.serviceAgent
  • Et avec l’activation de Container Registry, ajouter au projet le compte de service service-<project_number>@containerregistry.iam.gserviceaccount.com  avec le rôle roles/editor

Cet ensemble d’actions induites par l’activation de service ne sont pas gérées par terraform il faut donc être vigilant de ne pas supprimer ces ressources nécessaires au bon fonctionnement du service

La création de « folder » est similaire mais en  plus simple :

  • Le folder doit être rattaché à un parent, organisation ou folder.
  • Il doit avoir un ID unique dans le parent où il se trouve, maximum 30 caractères alphanumériques, espace, underscore, quote simple et  tiret.
  • Maximum 10 niveaux hiérarchiques

Pour créer un folder à la racine de l’organisation :

data "google_organization" "org" {
  domain = var.org_domain
}
 
resource "google_folder" "my_folder" {
  display_name = "my_folder"
  parent       = data.google_organization.org.name
}

Pour faire cela je dois avoir les 2 rôles suivants (au minimum) :

  • roles/resourcemanager.organizationViewer pour pouvoir récupérer l’identifiant de l’organisation avec la datasource google_organization
  • foles/resourcemanager.folderCreator au niveau du parent, donc dans cet exemple, l’organisation

Gestion des permissions IAM

Avant de se lancer dans la gestion des permissions avec Terraform il faut avoir intégré la notion d’héritage des permissions sur GCP.

Il est possible d’ajouter des rôles, sur

  • L’organisation
  • un Folder
  • un Projet
  • Certaine ressource (Bucket par exemple)

Un projet va hériter des permissions des parents  :

Dans terraform il existe donc des ressources terraform pour chaque niveau de permission :

  • Organisation => google_organization_iam_*
  • Folder => google_folder_iam_*
  • Project => google_project_iam_*
  • Bucket => google_storage_bucket_iam_*
  • Instance compute engine => google_compute_instance_iam_*

Ces ressources terraform sont déclinées en 3 catégories qui vont avoir des comportements différents :

  • policy :
    On définit tous les rôles avec cette ressource terraform qui va synchroniser exactement ce qui se trouve dans terraform avec les permissions sur Google Cloud Platform. Cela nous assure qu’aucun autre rôle ne peut être ajouté ou qu’il sera supprimé dès qu’on fera le ‘apply terraform’. On parle de ressource « Authoritative ». Généralement compliqué et parfois dangereux à utiliser, notamment dans le cas des permissions d’un projet car un certain nombre de rôles sont attribués de manière automatique par la plateforme et donc peuvent être écrasés par terraform ce qui peut créer des dysfonctionnements.
    Je vous conseille de lire les mises en garde associées, dans la documentation, avant de l’utiliser par exemple pour google_project_iam_policy.
    Exemple pour un projet : 

    resource "google_project_iam_policy" "project" {
      project     = google_project.my_project_in_a_folder.project_id
      policy_data = data.google_iam_policy.iam_policy.policy_data
    }
     
    data "google_iam_policy" "iam_policy" {
      binding {
        role = "roles/viewer"
     
        members = [
          "user:user@xxxxx.xx",
        ]
      }
     
      binding {
        role = "roles/owner"
     
        members = [
          "group:admin@xxxxxx.xxx",
        ]
      }
    }

    Dans l’exemple ci dessus le contenu de iam_policy est l’équivalent de ce qu’on aurait avec la commande gcloud projects get-iam-policy 

  • binding: 
    On définit tous les membres d’un seul rôle, qui seront synchronisés lors du ‘apply terraform’. Assure que ce rôle est donné uniquement à la liste de membres fournie. On parle de ressource « Authoritative » par rôle.
    Exemple pour un folder :

    resource "google_folder_iam_binding" "folder_viewers" {
      folder = google_folder.my_folder.name
      role   = "roles/viewer"
     
      members = [
        "user:user@xxxxx.xx",
      ]
    }
    resource "google_folder_iam_binding" "folder_owners" {
      folder = google_folder.my_folder.name
      role   = "roles/owner"
     
      members = [
        "group:admin@xxxxxx.xxx",
      ]
    }

     

  • Member : 
    On définit un ajout de rôle pour un membre donné. On parle de ressource « non Authoritative ». Ne supprime pas les rôles qui auraient été ajoutés sans terraform.
    Exemple pour un bucket :

    resource "google_storage_bucket_iam_member" "user_can_view_simple_bucket_object" {
      bucket = google_storage_bucket.simple-bucket.name
      role = "roles/storage.objectViewer"
      member =  "user:user@xxxxx.xx",
    }
    resource "google_storage_bucket_iam_member" "admin_is_storage_admin_in_simple_bucket" {
      bucket = google_storage_bucket.simple-bucket.name
      role = "roles/storage.admin"
      member =  "group:admin@xxxxxx.xxx",
    }

     

Tips : google_project_iam_policy et Kubernetes Engine

Comme vu précédemment au sujet de l’activation des services, activer GKE ajoutera automatiquement des comptes de service techniques ainsi que des rôles associés. Utiliser google_project_iam_policy dans ces conditions va supprimer les rôles des comptes de service et bloquer le service GKE. Le code suivant :

resource "google_project_iam_policy" "project" {
  project     = google_project.my_project_in_a_folder.project_id
  policy_data = data.google_iam_policy.admin.policy_data
}
 
data "google_iam_policy" "admin" {
  binding {
    role = "roles/viewer"
 
    members = [
      "user:user@xxxx.xxx",
    ]
  }
 
  binding {
    role = "roles/owner"
 
    members = [
      "group:admin-group@xxxxxx.xx",
    ]
  }
}
 
locals {
  services = ["container.googleapis.com"]
}
 
resource "google_project_service" "services" {
  for_each = toset(local.services)
  project = google_project.my_project_in_a_folder.project_id
  service = each.value
 
}

Lors du premier apply rien d’anormal : 

  • Ajout des rôles
  • Activation du service GKE 

Mais si je fais un second apply terraform souhaitera supprimer les mappings rôles/comptes de service suivant : 

  • roles/compute.serviceAgent => service-<project_number>@compute-system.iam.gserviceaccount.com 
  • roles/editor => [service-<project_number>@containerregistry.iam.gserviceaccount.com, <project_number>-compute@developer.gserviceaccount.com, <project_number>@cloudservices.gserviceaccount.com]

Appliquer ces modifications entrainera l’impossibilité du service de fonctionner, il ne sera plus possible, non plus, de créer des clusters ou en supprimer 

2 solutions possibles : 

  • Ajouter ces rôles dans google_iam_policy après avoir activé le service mais ce n’est clairement pas recommandé car c’est juste une correction manuelle
  • Utiliser google_project_iam_binding pour attribuer ces rôles en faisant bien attention de ne pas l’utiliser sur roles/editor et roles/compute.serviceAgent

Conclusion

Terraform est un bon choix pour gérer son infrastructure sur Google Platform. Cependant, même si au premier abord cela semble très simple il reste nécessaire de comprendre un minimum le fonctionnement  des services, des permissions IAM et de l’organisation des resources.

Il est également recommandé de comprendre comment Terraform agit et de vérifier ce que l’on modifie avec un « terraform plan »

Dans cet article, nous avons exploré un certain nombre de concepts et également de pièges à éviter avec les ressources de base cependant il existe aussi des modules de plus hauts niveaux maintenus  par Google pour simplifier la configuration de certains services : https://registry.terraform.io/modules/terraform-google-modules.

Published by

Publié par Ivan Beauvais

Consultant chez Xebia/Publicis Sapient Engineering depuis 7 ans. Je m'intéresse à l'écosystème Java/Scala, le Cloud sur Google Cloud Platform, les containers, Elasticsearch, Kafka pour des problématiques Data et Web

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.