Maison > Article > développement back-end > Comment nous utilisons les tâches Kubernetes pour faire évoluer OpenSSF Scorecard
Nous avons récemment publié des intégrations avec OpenSSF Scorecard sur la plateforme OpenSauced. L'OpenSSF Scorecard est une puissante interface de ligne de commande Go que tout le monde peut utiliser pour commencer à comprendre l'état de sécurité de ses projets et de ses dépendances. Il effectue plusieurs vérifications des flux de travail dangereux, des meilleures pratiques CICD, si le projet est toujours maintenu, et bien plus encore. Cela permet aux créateurs de logiciels et aux consommateurs de comprendre leur situation globale en matière de sécurité, de déduire si un projet est sûr à utiliser et où des améliorations doivent être apportées aux pratiques de sécurité.
Mais l'un de nos objectifs en intégrant l'OpenSSF Scorecard dans la plateforme OpenSauced était de le rendre disponible à l'écosystème open source plus large dans son ensemble. S'il s'agit d'un référentiel sur GitHub, nous voulions pouvoir afficher une partition pour celui-ci. Cela signifiait faire évoluer la CLI Scorecard pour cibler presque tous les référentiels sur GitHub. Beaucoup plus facile à dire qu'à faire !
Dans cet article de blog, expliquons comment nous avons fait cela en utilisant Kubernetes et quelles décisions techniques nous avons prises lors de la mise en œuvre de cette intégration.
Nous savions que nous aurions besoin de créer un microservice de type cron qui mettrait fréquemment à jour les scores dans une myriade de référentiels : la vraie question était de savoir comment procéder. Cela n'aurait pas de sens d'exécuter la CLI de scorecard de manière ad hoc : la plate-forme pourrait trop facilement être submergée et nous voulions pouvoir effectuer une analyse plus approfondie des scores dans l'ensemble de l'écosystème open source, même si la page du dépôt OpenSauced n'a pas été visité récemment. Initialement, nous avons envisagé d'utiliser la bibliothèque Scorecard Go comme code dépendant directement et d'exécuter des vérifications de carte de score au sein d'un microservice unique et monolithique. Nous avons également envisagé d'utiliser des tâches sans serveur pour exécuter des conteneurs de cartes de pointage uniques qui renverraient les résultats pour des référentiels individuels.
L'approche sur laquelle nous avons fini par atterrir, qui allie simplicité, flexibilité et puissance, consiste à utiliser les tâches Kubernetes à grande échelle, le tout géré par un microservice de contrôleur Kubernetes « planificateur ». Au lieu de construire une intégration de code plus approfondie avec scorecard, l'exécution de tâches Kubernetes uniques nous offre les mêmes avantages que l'utilisation d'une approche sans serveur, mais à un coût réduit puisque nous gérons tout directement sur notre cluster Kubernetes. Les tâches offrent également une grande flexibilité dans leur exécution : elles peuvent avoir des délais d'attente longs et étendus, elles peuvent utiliser un disque et, comme tout autre paradigme Kubernetes, elles peuvent avoir plusieurs pods effectuant différentes tâches.
Décomposons les composants individuels de ce système et voyons comment ils fonctionnent en profondeur :
La première et la plus grande partie de ce système est le « scorecard-k8s-scheduler » ; un microservice de type contrôleur Kubernetes qui lance de nouvelles tâches sur le cluster. Bien que ce microservice suive de nombreux principes, modèles et méthodes utilisés lors de la création d'un contrôleur ou d'un opérateur Kubernetes traditionnel, il ne surveille ni ne mute les ressources personnalisées sur le cluster. Sa fonction est simplement de lancer les tâches Kubernetes qui exécutent la CLI Scorecard et de rassembler les résultats des tâches terminées.
Regardons d'abord la boucle de contrôle principale dans le code Go. Ce microservice utilise la bibliothèque Kubernetes Client-Go pour s'interfacer directement avec le cluster sur lequel le microservice s'exécute : ceci est souvent appelé configuration et client sur le cluster. Dans le code, après avoir démarré le client sur le cluster, nous recherchons les référentiels de notre base de données qui nécessitent une mise à jour. Une fois que certains dépôts sont trouvés, nous lançons les tâches Kubernetes sur des « threads » de travailleurs individuels qui attendront la fin de chaque tâche.
// buffered channel, sort of like semaphores, for threaded working sem := make(chan bool, numConcurrentJobs) // continuous control loop for { // blocks on getting semaphore off buffered channel sem <- true go func() { // release the hold on the channel for this Go routine when done defer func() { <-sem }() // grab repo needing update, start scorecard Kubernetes Job on-cluster, // wait for results, etc. etc. // sleep the configured amount of time to relieve backpressure time.Sleep(backoff) }() }
Cette méthode de « boucle de contrôle infinie », avec un canal tamponné, est un moyen courant dans Go de faire quelque chose en continu mais en utilisant uniquement un nombre configuré de threads. Le nombre de fonctions Go simultanées exécutées à un moment donné dépend de la valeur configurée de la variable « numConcurrentJobs ». Cela configure le canal mis en mémoire tampon pour agir comme un pool de travailleurs ou un sémaphore qui indique le nombre de fonctions Go simultanées exécutées à un moment donné. Étant donné que le canal tamponné est une ressource partagée que tous les threads peuvent utiliser et inspecter, j'aime souvent le considérer comme un sémaphore : une ressource, un peu comme un mutex, sur laquelle plusieurs threads peuvent tenter de se verrouiller et d'accéder. Dans notre environnement de production, nous avons augmenté le nombre de threads de ce planificateur exécutés en même temps. Étant donné que le planificateur lui-même n'est pas très lourd en termes de calcul et qu'il se contente de lancer des tâches et d'attendre que les résultats finissent par apparaître, nous pouvons repousser les limites de ce que ce planificateur peut gérer. Nous disposons également d'un système de backoff intégré qui tente de soulager la pression en cas de besoin : ce système incrémentera la valeur de "backoff" configurée s'il y a des erreurs ou si aucun dépôt n'est trouvé pour calculer le score. Cela garantit que nous ne bombardons pas continuellement notre base de données avec des requêtes et que le planificateur de cartes de score lui-même peut rester dans un état « d'attente », sans utiliser de précieuses ressources de calcul sur le cluster.
Au sein de la boucle de contrôle, nous faisons plusieurs choses : tout d'abord, nous interrogeons notre base de données pour connaître les référentiels dont la carte de score est mise à jour. Il s'agit d'une simple requête de base de données basée sur certaines métadonnées d'horodatage que nous surveillons et sur lesquelles nous avons des index. Une fois qu'un laps de temps configuré s'est écoulé depuis que le dernier score a été calculé pour un dépôt, il bouillonne pour être analysé par une tâche Kubernetes exécutant la CLI Scorecard.
Ensuite, une fois que nous avons un dépôt pour lequel obtenir le score, nous lançons une tâche Kubernetes en utilisant l'image « gcr.io/openssf/scorecard ». L'amorçage de ce travail dans le code Go à l'aide de Client-Go ressemble beaucoup à ce à quoi il ressemblerait avec yaml, en utilisant simplement les différentes bibliothèques et API disponibles via les importations « k8s.io » et en le faisant par programme :
// defines the Kubernetes Job and its spec job := &batchv1.Job{ // structs and details for the actual Job // including metav1.ObjectMeta and batchv1.JobSpec } // create the actual Job on cluster // using the in-cluster config and client return s.clientset.BatchV1().Jobs(ScorecardNamespace).Create(ctx, job, metav1.CreateOptions{})
Une fois le travail créé, nous attendons qu'il signale qu'il est terminé ou erroné. Tout comme avec kubectl, Client-Go offre un moyen utile de « surveiller » les ressources et d'observer leur état lorsqu'elles changent :
// watch selector for the job name on cluster watch, err := s.clientset.BatchV1().Jobs(ScorecardNamespace).Watch(ctx, metav1.ListOptions{ FieldSelector: "metadata.name=" + jobName, }) // continuously pop off the watch results channel for job status for event := range watch.ResultChan() { // wait for job success, error, or other states }
Enfin, une fois le travail terminé avec succès, nous pouvons récupérer les résultats des journaux de pod du travail qui contiendront les résultats json réels de la CLI de la carte de score ! Une fois que nous avons ces résultats, nous pouvons réinsérer les scores dans la base de données et muter toutes les métadonnées nécessaires pour signaler à nos autres microservices ou à l'API OpenSauced qu'il y a un nouveau score !
Comme mentionné précédemment, le scorecard-k8s-scheduler peut exécuter n'importe quel nombre de tâches simultanées en même temps : dans notre environnement de production, nous avons un grand nombre de tâches exécutées en même temps, toutes gérées par ce microservice. L'intention est de pouvoir mettre à jour les scores toutes les 2 semaines dans tous les référentiels sur GitHub. Avec ce type d'échelle, nous espérons être en mesure de fournir des outils et des informations puissants à tout responsable ou consommateur open source !
Le microservice « planificateur » finit par être une petite partie de tout ce système : toute personne familiarisée avec les contrôleurs Kubernetes sait qu'il existe des éléments supplémentaires de l'infrastructure Kubernetes qui sont nécessaires pour faire fonctionner le système. Dans notre cas, nous avions besoin d'un contrôle d'accès basé sur les rôles (RBAC) pour permettre à notre microservice de créer des tâches sur le cluster.
Tout d'abord, nous avons besoin d'un compte de service : c'est le compte qui sera utilisé par le planificateur et auquel seront liés des contrôles d'accès :
apiVersion: v1 kind: ServiceAccount metadata: name: scorecard-sa namespace: scorecard-ns
Nous plaçons ce compte de service dans notre espace de noms « scorecard-ns » où tout cela se déroule.
Ensuite, nous devons avoir un rôle et une liaison de rôle pour le compte de service. Cela inclut les contrôles d'accès réels (y compris la possibilité de créer des tâches, d'afficher les journaux des pods, etc.)
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: scorecard-scheduler-role namespace: scorecard-ns rules: - apiGroups: ["batch"] resources: ["jobs"] verbs: ["create", "delete", "get", "list", "watch", "patch", "update"] - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"] — apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: scorecard-scheduler-role-binding namespace: scorecard-ns subjects: - kind: ServiceAccount name: scorecard-sa namespace: scorecard-ns roleRef: kind: Role name: scorecard-scheduler-role apiGroup: rbac.authorization.k8s.io
Vous vous demandez peut-être : « Pourquoi dois-je donner accès à ce compte de service pour obtenir des pods et des journaux de pod ? N’est-ce pas une extension excessive des contrôles d’accès ? » Souviens-toi! Les tâches ont des pods et pour obtenir les journaux de pods qui contiennent les résultats réels de la CLI du tableau de bord, nous devons être capables de lister les pods d'une tâche, puis de lire leurs journaux !
La deuxième partie, le « RoleBinding », est l'endroit où nous attachons réellement le rôle au compte de service. Ce compte de service peut ensuite être utilisé lors du lancement de nouvelles tâches sur le cluster.
—
Un grand merci à Alex Ellis et à son excellent contrôleur d'exécution de tâches : ce fut une énorme source d'inspiration et de référence pour utiliser correctement Client-Go avec Jobs !
Restez coquins tout le monde !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!