Introduction
Je vous ai présenté dans un premier article comment (et pourquoi) une représentation graphe de nos ressources nous avait permis de faciliter l’administration de notre IAM sur GCP. Dans cette suite, je vous propose de revenir sur la solution technique mise en place pour arriver à ce résultat. Nous allons rentrer ici en détail dans l’architecture, en expliquant chaque choix, et en montrant le cheminement qui nous a permis d’arriver à la solution finale.
Une des règles principales fixée dès le début de cette aventure était de se reposer au maximum sur les services managés que nous propose Google Cloud, afin de minimiser les développements.
Sans plus tarder, voici un schéma macroscopique de l’architecture, représentant les différentes briques utilisées et les interactions entre celles-ci :
Ce schéma ne vous semble probablement pas clair au premier coup d’œil et c’est normal : il s’agit d’un schéma global, autour duquel gravitera la suite de l’article. Continuez à lire pour le comprendre! Pour faciliter la lecture de l’article, la solution technique sera découpée en 4 étapes successives, chacune des étapes étant nécessaire pour passer à la suivante :
- récupération des ressources
- traitements de normalisation
- stockage du résultat normalisé
- chargement des ressources dans la base de données graphe
Étape 1/4 : récupération des ressources
Pour représenter nos ressources sous forme d’un graphe, il faut bien entendu disposer de la liste de ces ressources ! La première étape consiste donc à récupérer toutes les informations intéressantes sur les ressources GCP dont nous avons besoin :
- organization
- folders
- projets
- IAM policies (sur toutes les ressources précédentes)
- IAM sur les autres ressources (datasets, buckets), ainsi que les particularités liées à chacune (ACL pour les buckets par exemple)
La récupération de toutes ces informations en temps réel est quasi impossible : comment être notifié instantanément de la création d’une ressource ou d’une modification de l’IAM?
En contrôlant nos ressources via un outil d’infra-as-code (Terraform par exemple, que nous utilisons par ailleurs), il serait possible d’être alerté de chaque modification sur l’infrastructure. Cela nécessiterait malgré tout de mettre en place une mécanique particulière : dans l’idée, chaque terraform apply
pourrait nous permettre de détecter les ressources modifiées et de synchroniser notre liste des ressources. Même en supposant que cela soit simple à mettre en place, tous les projets de notre organisation n’utilisent pas Terraform (malheureusement!), rendant donc cette solution caduque.
La solution va donc consister à récupérer cette liste de ressources périodiquement (batch). Deux alternatives sont à notre disposition :
- interroger les différentes APIs (IAM, datasets, buckets, …) avec du code « fait maison »
- réutiliser un composant existant : Cloud Security Command Center (CSCC), ou Forseti (que nous utilisons par ailleurs pour la vérification de règles, voir ci-dessous pour une présentation plus détaillée)
La première solution a tout de suite été écartée : rappelons qu’une des règles fixées au départ était de minimiser les développements. Cette solution aurait nécessité de maintenir du code propre à chaque type de ressource. En plus d’être sujet aux erreurs (plus de code = plus de bugs !), il est souvent plus judicieux de ne pas réinventer la roue, mais plutôt de se reposer sur des solutions existantes, comme dans le second point.
Les deux alternatives (CSCC ou Forseti) se valent ici : il faut aussi savoir que CSCC peut se baser sur Forseti pour réaliser l’inventaire des ressources. Nous avons néanmoins préféré Forseti, car CSCC semble moins à jour dans la liste des ressources, d’après nos expérimentations (nous avons déjà eu le cas d’un bucket qui n’existait plus et qui apparaissait pourtant dans CSCC).
Forseti
Le rôle de Forseti est multiple. Cette application, facilement déployable sur Compute Engine, se charge principalement de :
- faire périodiquement des inventaires des ressources GCP
- assurer la conformité (compliance) en vérifiant des règles sur les ressources et en alertant en cas de violation (par exemple, alerter si un bucket est créé aux US, sur certains projets)
C’est le premier point qui va nous intéresser ici.
Inventaire des ressources
Toutes les 2 heures (valeur par défaut, cet intervalle est configurable), Forseti lance un inventaire des ressources GCP sur l’organisation, et stocke le résultat (la liste des ressources) dans sa propre base Cloud SQL.
On ne peut pas dire que les tables dans cette base SQL soient des plus normalisées (voir ci-dessous), mais cela s’avère tout de même utile. Nous n’avons pas besoin de récupérer dans ces tables toutes les ressources de l’inventaire exhaustivement (par exemple, nous ne sommes pas intéréssés par les firewall rules, compute disks, …).
À la fin de chaque inventaire, il est intéressant de remarquer que Forseti crée sur Google Cloud Storage un fichier contenant un résumé de l’inventaire (nombre de ressources par type de ressource). Le contenu de ce fichier ne nous intéresse pas; en revanche, cet événement de création de fichier pourra être utilisé pour déclencher nos traitements.
Données Forseti
Revenons sur le contenu de la base Cloud SQL utilisée par Forseti. Celle-ci contient 2 tables, dans une base de données forseti_security
:
forseti_security.inventory_index
: Forseti va y stocker 1 ligne par inventaire (date de l’inventaire, identifiant, …)forseti_security.gcp_inventory
: Forseti va y stocker 1 ligne par ressource par inventaire. Chaque ligne est associée à un inventaire via son identifiant (stocké dansforseti_security.inventory_index
). Dans cette table, c’est le champresource_data
qui va le plus nous intéresser : on y retrouve la description de chaque ressource, stockée au format JSON. Pour les plus curieux, la structure complète de cette table est décrite ci-dessous :
MySQL [forseti_security]> describe forseti_security.gcp_inventory; +--------------------+-------------------------------------------------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +--------------------+-------------------------------------------------------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | inventory_index_id | bigint(20) | YES | MUL | NULL | | | category | enum('resource','iam_policy','gcs_policy','dataset_policy', | YES | | NULL | | | | 'billing_info','enabled_apis','kubernetes_service_config') | | | | | | resource_type | varchar(255) | YES | | NULL | | | resource_id | text | YES | | NULL | | | resource_data | longtext | YES | | NULL | | | parent_id | int(11) | YES | MUL | NULL | | | other | text | YES | | NULL | | | inventory_errors | text | YES | | NULL | | +--------------------+-------------------------------------------------------------+------+-----+---------+----------------+
Voici un extrait de cette table (3 lignes au format CSV) :
9720829,1552047865258278,"resource","project","<project_id>","{""createTime"": ""2018-12-04T14:46:32.468Z"", ""name"": ""<project_name>"", ""parent"": {""id"": ""<parent_folder_id>"", ""type"": ""folder""}, ""projectId"": ""<project_id>"", ""projectNumber"": ""<project_number>""}",9720214,"{""timestamp"": ""2019-03-08T12:24:37""}","" 9720191,1552047865258278,"iam_policy","project","<project_id>","{""bindings"": [, {""members"": [""serviceAccount:<service_account_email>""], ""role"": ""roles/owner""}, {""members"": [""group:<group_email>"", ""user:<user_email>""], ""role"": ""roles/storage.objectAdmin""}], ""etag"": ""BwWU="", ""version"": 1}",9720184,"{""timestamp"": ""2019-03-08T12:24:30""}",NULL 9720210,1552047865258278,"resource","bucket","<bucket_name>","{""acl"": [{""bucket"": ""<bucket_name>"", ""entity"": ""project-editors-<project_id>"", ""etag"": ""CAM="", ""id"": ""<bucket_name>/project-editors-<project_id>"", ""kind"": ""storage#bucketAccessControl"", ""projectTeam"": {""projectNumber"": ""<project_id>"", ""team"": ""editors""}, ""role"": ""OWNER"", ""selfLink"": ""https://www.googleapis.com/storage/v1/b/<bucket_name>/acl/project-editors-<project_id>""}, ...}], ""defaultObjectAcl"": [{""entity"": ""project-owners-<project_id>"", ""etag"": ""CAM="", ""kind"": ""storage#objectAccessControl"", ""projectTeam"": {""projectNumber"": ""<project_id>"", ""team"": ""owners""}, ""role"": ""OWNER""}], ""etag"": ""CAM="", ""iamConfiguration"": {""bucketPolicyOnly"": {""enabled"": false}}, ""id"": ""<bucket_name>"", ""kind"": ""storage#bucket"", ""location"": ""EU"", ""metageneration"": ""3"", ""name"": ""<bucket_name>"", ""owner"": {""entity"": ""project-owners-<project_id>""}, ""projectNumber"": ""<project_id>"", ""selfLink"": ""https://www.googleapis.com/storage/v1/b/<bucket_name>"", ""storageClass"": ""MULTI_REGIONAL"", ""timeCreated"": ""2019-02-07T15:47:49.608Z"", ""updated"": ""2019-02-19T14:02:55.042Z""}",9720185,"{""timestamp"": ""2019-03-08T12:24:35""}",""
Plutôt illisible? Si on y prête tout de même attention, on voit que le champ resource_data
(sixième champ) contient la description de la ressource, mais n’est pas manipulable tel quel : c’est un objet JSON représentant la ressource GCP, tel qu’on peut le récupérer lorsqu’on passe par la CLI ou par les APIs. Ce champ ne peut pas être manipulé directement avec du SQL car la base Cloud SQL de Forseti est obligatoirement un MySQL, et que MySQL ne dispose pas de fonctions avancées pour parser du JSON. Quoi qu’il en soit, il n’est même pas sûr qu’il eût été judicieux de parser du JSON avec du SQL (avis personnel).
Résumé
En résumé, la première étape consiste à récupérer la liste des ressources présentes à travers notre organisation. Pour cela, nous avons fait le choix de Forseti, qui se charge de réaliser un tel inventaire des ressources toutes les deux heures. Les données de l’inventaire sont placées dans une base Cloud SQL, et un évènement de création de fichier nous indique lorsqu’un inventaire a été effectué.
Étape 2/4 : traitements de normalisation
Pour pouvoir exploiter les données de la base Forseti, il faut tout d’abord remettre en forme ces données.
Comment normaliser?
Plutôt que de traiter directement la donnée dans Cloud SQL, nous avons fait le choix de passer par des fichiers CSV intermédiaires, dans lesquels les données seront normalisées. Cet intermédiaire permet aussi de séparer les traitements remise en forme des données et insertion dans une base graphe. Ce découplement pourra aussi permettre de charger les données vers d’autres destinations…. Le choix de passer par ces fichiers intermédiaires apparaîtra plus clair dans la suite de cet article.
En résumé, cette étape revient à éclater notre table SQL en plusieurs fichiers (un CSV normalisé pour chaque resource_type : IAM policy, project, bucket, …) où les données issues du champ resource_data
(JSON) ont été remises à plat. Pour éviter des traitements inutiles, seuls les champs nécessaires de ce JSON seront conservés.
Ci-dessous se trouve un extrait de fichier CSV concernant l’IAM policy sur les projets :
accountType,email,role,projectId group,a_group@myorganization.com,roles/editor,<project_id> serviceAccount,<project_id>-compute@developer.gserviceaccount.com,roles/editor,<project_id> user,a_simple_user@myorganization.com,roles/editor,<project_id>
En sortie de cette étape de normalisation, nous disposons donc de 11 CSV, chacun associé à une resource_type
. Chaque fichier possède plusieurs colonnes, selon le type de ressource. Voici la liste complète de ces fichiers, accompagnés de leur header. On notera aussi que chaque nom de fichier respecte une convention particulière basée sur 2 champs de la table gcp_inventory
(-.csv)
:
$ find * -name '*.csv' | xargs -I '{}' sh -c 'printf "%-30s " {}; head -1 {}' dataset_policy-dataset.csv id,role,policyType,policyEntity iam_policy-bucket.csv id,role,accountType,account iam_policy-folder.csv accountType,email,role,folderId iam_policy-organization.csv accountType,email,role,organizationId iam_policy-project.csv accountType,email,role,projectId resource-bucket.csv id,projectNumber,location,storageClass,timeCreated,bucketPolicyOnlyLocked resource-dataset.csv id,projectId,datasetId,location resource-folder.csv id,name,displayName,parent resource-organization.csv id,name,displayName,creationTime,ownerDirectoryCustomerId resource-project.csv id,projectNumber,projectId,name,parentId resource-serviceaccount.csv id,name,email,displayName,projectId
Les noms des colonnes sont similaires aux noms que l’on retrouve dans le JSON du champ resource_data
, afin d’éviter toute confusion.
Quand et comment exécuter le traitement?
Comme présenté plus tôt, Forseti dépose à chaque inventaire un fichier « résumé » sur GCS. Nous pouvons donc utiliser cet événement de dépôt de fichier comme déclencheur de notre traitement. Basé sur cette remarque, c’est donc naturellement qu’une Cloud Function a été choisie pour effectuer ce traitement. La normalisation des données sera effectuée en Go, seul langage typé supporté à ce jour par Cloud Functions, et qui nous permet de manipuler facilement la donnée avec des structures.
L’avantage de cette solution est d’éviter de payer une instance allumée toute la journée pour une action à faire seulement toutes les 2 heures. L’inconvénient est que le temps de traitement doit être inférieur à 9 minutes (timeout), mais cela devrait suffire car ici on ne parle que de mise en forme de données. Chaque traitement ne récupérera que les données du dernier inventaire (celui auquel est associé le fichier déclencheur).
Ressources additionnelles
Mapping groupes – utilisateurs
L’ensemble des CSV présentés au-dessus est nécessaire, mais pas suffisant. Comme indiqué dans le précédent article, nous avons fait reposer une grande partie de l’IAM sur des groupes, plutôt que de donner des droits sur des utilisateurs nominativement : utiliser des groupes est d’ailleurs une bonne pratique poussée par Google. Tous nos groupes suivent une convention de nommage (gcp_***@myorganization.com), il est donc très simple de lister tous les groupes que nous gérons, et de récupérer l’ensemble de leurs membres via un appel à GSuite (voir cet exemple de code pour utiliser l’admin SDK GSuite en Go).
Rajoutons donc le fichier suivant à la liste précédente :
mapping_groups.csv group,user
Chaque ligne de ce fichier sera un tuple (g, u), où u est membre du groupe g.
Liste des permissions?
Intégrer la liste des permissions associée à chaque rôle aurait aussi pu être intéressant. En effet, l’IAM policy nous indique seulement quel rôle l’utilisateur/groupe/service account possède sur une ressource, mais pas directement ses permissions, chaque rôle étant une collection de permissions.
Cependant, dans notre organisation, la décision a été prise de n’utiliser que des rôles prédéfinis (pour des raisons de simplicité de gestion des droits, qui est déjà suffisamment fastidieuse). Ce mapping n’était donc, dans notre cas, pas vraiment important. Il pourra cependant être rajouté dans une seconde version de l’application si le besoin se présente.
Résumé
En résumé, l’évènement de création de fichier produit par Forseti va nous servir à déclencher notre traitement de normalisation (Cloud Function). Après avoir récupéré la liste des ressources du dernier inventaire dans la base Cloud SQL de Forseti, une liste de fichiers normalisés est produite (un fichier CSV par type de ressource).
Étape 3/4 : stockage du résultat normalisé
En sortie de l’étape de normalisation, nous avons donc plusieurs fichiers CSV nettoyés : mise à plat des données imbriquées, contenu remis en forme, filtre sur certains champs.
Gardons tout de même en tête que l’objectif est de mettre ces données dans une base de données graphe ! Plusieurs solutions s’offrent à nous pour passer de ces données, normalisées, vers notre graphe :
- les données sont chargées dans une base de données graphe démarrée sur une instance Google Compute Engine (malheureusement, Google ne propose pas de service managé « base de données graphe » actuellement, contrairement à AWS avec son service Neptune)
- les données sont chargées directement dans une base de données embarquée : on pourrait générer une archive contenant la base, qui pourrait être déposée sur un bucket pour un chargement des données « à la demande »
- la création de la base de données graphe n’intervient pas au niveau du traitement (Cloud Function), mais est déléguée à un autre processus : ici, on sépare les responsabilités; mais tout comme la solution précédente, il faut alors déposer les fichiers CSV quelque part pour déclencher dans un autre processus la création de la base de données graphe
Dans ce cas précis – et aussi d’une manière générale – je suis plutôt favorable à la séparation des responsabilités :
- le code de traitement écrit jusqu’ici dans la Cloud Function n’est pas trivial, même si il ne s’agit principalement que de mise en forme : connexion à Cloud SQL, mapping des JSON vers des objets métiers, génération des CSV, appels à GSuite pour récupérer le mapping des groupes, …
- il est plus judicieux de séparer la mise en forme des données de la création de la base, car le traitement pourrait évoluer (modification de la source de données, correction d’une information dans un fichier CSV), sans pour autant à avoir à modifier la création de la base (si bien sûr le format d’échange n’a pas changé)
Mais qui dit séparation, dit communication : il faut trouver un endroit où stocker ces fichiers CSV intermédiaires. Là encore, plusieurs solutions s’offrent à nous :
- stockage sur GCS
- stockage sur un repository Git
GCS ou Git?
La première solution paraît la plus naturelle, mais Git peut s’avérer être une bonne idée car :
- cela permet de garder un historique. Bien que ce soit aussi possible avec GCS (Object Versioning), Git nous permet de bénéficier des outils comme
git diff
,git bisect
, …, qui seront très utiles pour détecter des modifications de nos CSV, et donc de nos ressources GCP - tout comme GCS, il existe la possibilité de déclencher un évènement lors d’un push sur un repository Git (via Cloud Build). Cet évènement déclenchera automatiquement le remplissage de notre base de données graphe. De plus :
- Git va nous permettre de ne faire un commit que si les CSV ont été modifiés (éviter les modifications inutiles)
- chaque nouvelle version des CSV aura un tag (correspondant à une date), pour pouvoir revenir facilement à un état de nos fichiers à un instant t. Il sera alors plus simple de trouver les modifications effectuées sur l’IAM entre 2 dates données
Au final, pour le stockage des CSV, GCS et Git proposent les mêmes fonctionnalités, mais la solution Git nous permet de bénéficier d’outils supplémentaires (croyez-moi, git bisect
est très utile pour retrouver à quel moment une modification sur les ressources a été effectuée !). C’est pourquoi nous avons opté pour le repository Git. Une fois les fichiers générés, la Cloud Function aura donc pour dernière action de pousser et tagger, si nécessaire, les fichiers modifiés dans Git :
Résumé
En résumé, les fichiers CSV issus de la normalisation seront stockés dans un repository Git. À chaque commit sera associé un tag (date du graphe), ce qui permettra de versionner facilement nos ressources. Les outils de Git (git diff
, git bisect
, …) nous permettront d’auditer simplement les modifications de nos ressources.
Étape 4/4 : chargement des ressources dans la base de données graphe
Une fois les fichiers CSV poussés sur le repository Git, chacune des lignes des fichiers sera traitée pour générer un ou plusieurs noeuds et/ou relations dans notre base de données graphe. Une base de données sera générée par tag Git, permettant ainsi de s’affranchir de gérer la complexité de devoir synchroniser nos noeuds/relations entre 2 versions (suppression des anciens noeuds/relations, …).
Choix de la base de données graphe
Le choix de la base de données n’a pas été très compliqué. Ayant déjà utilisé Neo4j, nous nous sommes tournés vers cet outil. En outre, rappelons les 3 raisons principales de choix, déjà évoquées dans l’article précédent :
- sa vaste adoption : Neo4j est sans aucun doute la base de données graphe la plus utilisée
- son langage de manipulation des données (Cypher)
- son interface graphique prête à l’emploi (Neo4j browser)
Chargement des données
Deux modes de chargement des fichiers CSV vers Neo4j sont à notre disposition :
La commande LOAD CSV
va nous permettre de faire gagner du temps et d’économiser des développements. C‘est aussi pour cette fonctionnalité que nous avons décidé de partir sur des fichiers CSV comme intermédiaire, et d’utiliser Neo4j.
Voici un exemple de commande Cypher de chargement de données, pour le fichier CSV iam_policy-project.csv
(IAM policy relative aux projets, dont un extrait a été présenté plus tôt) :
LOAD CSV WITH HEADERS FROM "file:///iam_policy-project.csv" AS row MERGE (a: Account {email: row.email}) ON CREATE SET a.accountType = row.accountType, a.email = row.email MERGE (p: Project {id: row.projectId}) MERGE (a)-[:HAS_ROLE {role: row.role}]->(p);
Pour chacune des lignes du fichier (row
), à l’exception du header, on va :
MERGE
r un noeud de typeAccount
, basé sur l’adresseemail
contenue dans la ligne (MERGE
est utilisé à la place deCREATE
pour ne pas créer plusieurs nœuds avec le mêmeemail
)- si ce noeud n’existe pas et qu’il est donc créé (
ON CREATE SET
), alors on va rajouter deux propriétésaccountType
etemail
, relatives à ce qui se trouve dans la ligne courante - on crée (ou
MERGE
si il existe déjà) un noeud de typeProject
, basé sur leprojectId
contenu dans la ligne - finalement, on crée (ou
MERGE
si il existe déjà) un lien de typeHAS_ROLE
représentant le lien entre le noeudAccount
et le noeudProject
, avec une propriétérole
contenant le rôle IAM (roles/bigquery.admin
par exemple)
À chacun des 12 fichiers listés plus tôt sera associé une requête de chargement, représentant au total environ 150 lignes de Cypher.
Mise à disposition de la base de données graphe
Une fois que les requêtes permettant de charger les données dans notre base Neo4j sont prêtes, la question est maintenant : comment mettre à disposition la base de données avec les données ? Bien qu’il soit possible de lancer manuellement les scripts à partir des CSV à chaque fois que l’on voudrait accéder à la base, il serait plus pratique que ces étapes soient automatisées.
Pour cela, nous avons choisi de générer une image de conteneur pour chacun des tags de notre repository Git (associé à une version de notre graphe). Cette image contiendra une base Neo4j (le runtime), ainsi que les données. Entre deux images, la seule différence sera donc le layer contenant les données. Cette solution est plus simple que la gestion d’un volume contenant les données et son partage : l’image contient directement tout ce qui est nécessaire (runtime + données). L’image de base sera l’image Neo4j officielle, ce qui permettra facilement de mettre à jour la version. Les images résultantes seront donc légères (seules les données changeront) et facilement utilisables.
Pour déclencher automatiquement le build d’une image de conteneur, nous avons utilisé Cloud Build, qui permet de réagir sur évènement : ce dernier étant la création d’un nouveau tag sur notre repository Git. En sortie, nous avons juste à publier une image dans Container Registry (registre d’images privé).
Pour Cloud Build, seul un fichier de configuration est nécessaire :
steps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'eu.gcr.io/$PROJECT_ID/<my-image>:$TAG_NAME', '.'] - name: 'gcr.io/cloud-builders/docker' args: ["push", "eu.gcr.io/$PROJECT_ID/<my-image>:$TAG_NAME"]
La première étape (docker build
) va construire l’image (Docker), à partir du Dockerfile
suivant, qui se trouve sur le repository Git, au même niveau que les fichiers CSV :
FROM neo4j:3.5.3 ENV NEO4J_AUTH=neo4j/<password> COPY cypher-import-scripts/*.cypher *.csv /var/lib/neo4j/import/ USER neo4j RUN echo "dbms.directories.data=/tmp" >> /var/lib/neo4j/conf/neo4j.conf \ && bin/neo4j-admin set-initial-password password || true \ && bin/neo4j start \ && sleep 15 \ && find /var/lib/neo4j/import/* -name '*.cypher' | sort | xargs -I{} /bin/sh -c "cat {}; echo" | \ NEO4J_USERNAME=neo4j NEO4J_PASSWORD=<password> \ /var/lib/neo4j/bin/cypher-shell --fail-fast
Sans trop rentrer dans le détail, on va dans un premier temps copier les fichiers CSV et les différents scripts Cypher d’intégration des données (cypher-import-scripts
, où nous avons un script par CSV) vers /var/lib/neo4j/import/
. Le processus Neo4j sera démarré lors du build (bin/neo4j start
), et, une fois prêt, tous les scripts Cypher seront ensuite exécutés grâce à cypher-shell
. L’authentification ici reste basique; mais nous n’avons pas le besoin de compliquer davantage, car les images sont stockées sur Container Registry, qui est privé.
La seconde étape (docker push
) consiste à pousser l’image résultante sur Container Registry (le même tag que Git est utilisé pour pouvoir retrouver facilement nos images avec une version donnée des ressources).
Résumé
En résumé, Cloud Build va réagir sur chaque push dans notre repository Git afin de produire une image de conteneur (Docker) versionnée par date. Le chargement des CSV se fait grâce à des requêtes Cypher. Chaque image Docker contient à la fois le runtime Neo4j et les données.
Conclusion
Concernant l’architecture, et comme vous avez pu le lire tout au long de cet article, sa création peut se résumer à une succession de choix techniques. L’avantage d’une solution Cloud est aussi de disposer de services managés, sur lesquels on peut se reposer pour minimiser les développements (citons ici Forseti, Storage, Cloud Function, Cloud Build, Container Registry). Sans avoir à réinventer la roue, mais plutôt en associant ces composants, nous avons réussi à créer rapidement une solution stable. Il est tout de même important d’avoir une vision globale de ce que l’on cherche à résoudre avant de se lancer, de bien décomposer en étape, et d’associer à chaque étape un outil selon le besoin et les contraintes (et non le contraire).
Sans paraphraser la conclusion du premier article, la base de données graphe résultante nous est aujourd’hui très utile, notamment grâce au versionnage des images de conteneur, qui nous permet de revenir dans le passé pour comprendre, investiguer, vérifier ou auditer.
Il reste malgré tout quelques points que l’on pourrait améliorer sur cette solution.
Le premier est l’exposition du conteneur contenant notre base de données graphe. En effet, pour rendre utilisable cette solution par un plus grand nombre au sein de notre organisation, il faudrait exposer cette base de données (du moins la dernière version) via un accès simple au Neo4j browser. Cela pose néanmoins de potentiels problèmes de sécurité : l’accès à cette base doit être contrôlé (utilisateurs, règles de firewall, …). La version entreprise de Neo4j propose une gestion des utilisateurs et des droits, mais nous sommes pour l’instant sur une licence standard. L’exposition du Neo4j browser pourrait être faite sur Compute Engine ou sur Kubernetes (GKE). Mais comment gérer facilement les différentes versions du graphe ? N’exposer que la dernière version ? Proposer une liste de versions et un démarrage à la volée ? Certaines questions restent en suspens.
Le second point concerne le repository Git. Actuellement, nous utilisons un repository externe (Bitbucket) car il est impossible (ou du moins pas documenté) d’avoir, pour un repository sur Cloud Source Repositories, une clé SSH pour un utilisateur machine (utilisé par la Cloud Function pour pousser sur Git). À terme, l’utilisation de Cloud Source Repositories pourrait nous permettre d’avoir une solution tournant à 100% sur des services Google Cloud.
Pour conclure, l’objectif est aussi de rendre cette solution open source. L’utilisation des services managés facilite la tâche de déploiement, mais l’installation nécessite tout de même d’avoir une certaine orchestration (Terraform ou Deployment Manager).
Dans un dernier article bonus, je montrerai comment cette architecture est évolutive et permet d’exposer ses données IAM vers d’autres destinations…