<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Onepoint x Stack Labs</title>
    <description>The latest articles on DEV Community by Onepoint x Stack Labs (@stack-labs).</description>
    <link>https://dev.to/stack-labs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1533%2F4d4f0f48-1dee-4525-9969-a7ba13bcb3ce.png</url>
      <title>DEV Community: Onepoint x Stack Labs</title>
      <link>https://dev.to/stack-labs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stack-labs"/>
    <language>en</language>
    <item>
      <title>Un Chatbot RAG pour explorer du contenu vidéo : une architecture event-driven et serverless sur Google Cloud</title>
      <dc:creator>Maximilien Soviche</dc:creator>
      <pubDate>Mon, 09 Dec 2024 16:02:09 +0000</pubDate>
      <link>https://dev.to/stack-labs/un-chatbot-rag-pour-explorer-du-contenu-video-une-architecture-event-driven-et-serverless-sur-5h4k</link>
      <guid>https://dev.to/stack-labs/un-chatbot-rag-pour-explorer-du-contenu-video-une-architecture-event-driven-et-serverless-sur-5h4k</guid>
      <description>&lt;p&gt;Un chatbot capable de répondre à vos questions tout en vous fournissant un &lt;strong&gt;lien cliquable vers la bonne vidéo YouTube&lt;/strong&gt;, et au bon endroit dans la vidéo. C'est ce que permet cette architecture conçue sur Google Cloud, mêlant &lt;strong&gt;IA générative&lt;/strong&gt; et &lt;strong&gt;approche serverless&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Dans cet article, nous explorerons comment mettre en place un chatbot RAG (&lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt;) qui utilise un corpus de vidéos pour fournir des réponses enrichies et pertinentes. Ce système repose sur une architecture &lt;strong&gt;event-driven&lt;/strong&gt; pilotée par &lt;strong&gt;EventArc&lt;/strong&gt; et des microservices déployés avec &lt;strong&gt;Cloud Run&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Une telle solution peut être appliquée dans divers contextes : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Formation&lt;/strong&gt; : Accès rapide à des segments vidéo éducatifs pertinents.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entreprise&lt;/strong&gt; : Recherche d'informations dans des vidéos internes comme des réunions ou des formations.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Médias et divertissement&lt;/strong&gt; : Exploration d'archives vidéo ou de contenus créatifs selon des requêtes contextuelles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parlement&lt;/strong&gt; : Exploration de contenus vidéos longs : séances, commissions, etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'application proposée ici est l'exploration d'une playlist Youtube de formation sur les services Google Cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  Qu'est ce que le RAG ?
&lt;/h2&gt;

&lt;p&gt;Le RAG veut dire &lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt;, et est la plupart du temps appliqué sur des documents textuels. Plutôt que de poser une question à un LLM, on va effectuer les étapes suivantes :&lt;/p&gt;

&lt;h3&gt;
  
  
  Ingestion
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;On prend des documents pertinents pour l'application souhaitée.&lt;/li&gt;
&lt;li&gt;On découpe ces documents en morçeaux, appelés &lt;strong&gt;chunks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;On construit une représentation de ces chunks appelée &lt;strong&gt;embedding&lt;/strong&gt;, lisible par un algorithme de recherche par similarité.&lt;/li&gt;
&lt;li&gt;On stocke ces embeddings dans une base de données.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Récupération
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;L'utilisateur pose une question qui est transformée en embedding.&lt;/li&gt;
&lt;li&gt;L'embedding est comparé à ceux de la base de données afin de retrouver du contexte pertinent.&lt;/li&gt;
&lt;li&gt;Un construit un prompt augmenté qui comporte la question de l'utilisateur ainsi que les éléments de contexte appropriés.&lt;/li&gt;
&lt;li&gt;Le LLM peut alors fournir une réponse pertinente sur ce prompt grâce au contexte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmeb072jdigqifckisszt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmeb072jdigqifckisszt.png" alt="RAG" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Le RAG sur du contenu vidéo
&lt;/h2&gt;

&lt;p&gt;Pour effectuer du RAG sur du contenu vidéo, l'idée principale est d'effectuer une transcription du contenu, afin de se ramener à un système RAG classique avec des documents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fef60ck3i505cbexpgsfh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fef60ck3i505cbexpgsfh.png" alt="RAG Video" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Vue d’ensemble de l’architecture
&lt;/h2&gt;

&lt;p&gt;Le système se divise en deux parties :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion des vidéos&lt;/strong&gt; : Un pipeline de traitement divise les vidéos en segments et génère des données prêtes pour la recherche.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval et Chatbot&lt;/strong&gt; : Le chatbot trouve les segments pertinents, fournit une réponse augmentée et un lien vers la vidéo correspondante.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8v92c94se9rkk1n525ud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8v92c94se9rkk1n525ud.png" alt="Architecture globale" width="800" height="1097"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Partie 1 : Ingestion des Vidéos
&lt;/h2&gt;

&lt;p&gt;L’ingestion repose sur cinq microservices Cloud Run. Ces services traitent les vidéos, segmentent leur contenu, et transforment les données en &lt;strong&gt;embeddings vectoriels&lt;/strong&gt; stockés dans Firestore pour la recherche. Chaque microservice produit un résultat dans un bucket spécifique, ce qui déclenche le traitement suivant via un déclencheur EventArc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4vwp2mn0160tfumxf6e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4vwp2mn0160tfumxf6e.png" alt="Architecture ingestion" width="800" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Étape 1 : Téléchargement des vidéos avec le service &lt;strong&gt;Downloader&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;L'utilisateur fournit une playlist YouTube via une API, et déclenche son téléchargement via un appel API sur ce service.&lt;/p&gt;

&lt;p&gt;Les métadonnées de la playlist (titres, descriptions, ID des vidéos) sont enregistrées dans une collection Firestore nommée &lt;code&gt;metadata&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Étape 2 : Segmentation des vidéos avec le service &lt;strong&gt;Splitter&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Chaque vidéo est découpée en segments de 2 minutes avec un recouvrement de 30 secondes. Ce recouvrement permet d'éviter la perte de contexte lorsqu'une phrase est coupée en deux. La piste audio est extraite pour chaque segment et stockée dans le bucket dédié.&lt;/p&gt;

&lt;p&gt;La collection &lt;code&gt;metadata&lt;/code&gt; dans Firestore est mise à jour. On associe à chaque vidéo ses segments avec leurs informations, notamment les timestamps de début et de fin.&lt;/p&gt;

&lt;p&gt;Le choix de la durée des segments (2 minutes) et de la taille de recouvrement peuvent être modifiés selon les besoins et le cas traité.&lt;/p&gt;

&lt;h3&gt;
  
  
  Étape 3 : Transcription des segments audio avec le service &lt;strong&gt;Transcriber&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Les pistes audio des segments sont converties en texte grâce à l’API Speech-to-Text de Google Cloud (modèle &lt;strong&gt;Chirp&lt;/strong&gt;). On obtient des transcriptions brutes dans un bucket.&lt;/p&gt;

&lt;p&gt;Chirp est un modèle de transcription speech-to-text de pointe, conçu pour fournir des services de transcription extrêmement précis et rapides. Il prend en charge une large gamme de langues et de dialectes, ce qui en fait un choix polyvalent pour répondre à divers besoins de transcription. Il est disponible via l'API Speech-to-Text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Étape 4 : Formatage des transcriptions avec le service &lt;strong&gt;Formatter&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Les transcriptions brutes sont corrigées pour améliorer la grammaire et la clarté grâce à &lt;strong&gt;Gemini 1.5 Flash&lt;/strong&gt;. On obtient ainsi des documents textuels dans un bucket, ce qui permet d'implémenter une logique RAG classique par la suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Étape 5 : Générer des embeddings avec le service &lt;strong&gt;Feeder&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Les transcriptions finales sont transformées en embeddings vectoriels à l’aide de &lt;strong&gt;LangChain&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Le service Firestore sert de base de données pour les embeddings, avec une collection dédiée nommée &lt;code&gt;embeddings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Chaque embedding est stocké avec l'identifiant du segment audio auquel il appartient, ce qui permettra de retrouver les métadonnées dans la collection &lt;code&gt;metadata&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Partie 2 : Retrieval et Chatbot
&lt;/h2&gt;

&lt;p&gt;Une fois le corpus indexé, le chatbot répond aux questions des utilisateurs en recherchant les segments pertinents dans les embeddings et en fournissant un lien précis vers la vidéo source. Le timestamp correspondra au point de départ du segment audio le plus pertinent.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Étapes du chatbot :&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Requête utilisateur&lt;/strong&gt; : L'utilisateur pose une question via Google Chat.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Recherche par similarité&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Les embeddings correspondant à la question sont récupérés dans Firestore.&lt;/li&gt;
&lt;li&gt;Les métadonnées associées (ID vidéo, segment, timestamps) sont extraites.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reranking&lt;/strong&gt; : La recherche par similarité permet d'obtenir les N chunks les plus pertinents (N=5 dans notre cas). L'étape de reranking consiste à utiliser un LLM pour réordonner ces chunks par ordre de pertinence, et n'en sélectionner qu'un.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Génération de réponse&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un prompt enrichi par le contexte du segment (le chunk retrouvé après reranking) est envoyé au LLM.&lt;/li&gt;
&lt;li&gt;Une URL est reconstruite avec le bon ID vidéo et le timestamp exact.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validation finale avec Gemini 1.5 Flash&lt;/strong&gt; : &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On ne veut pas que l'URL de la vidéo soit fournie pour chaque réponse, sinon une question qui n'a rien à voir avec le contexte aboutirait quand même vers le contenu le moins éloigné en termes de similarité.&lt;/p&gt;

&lt;p&gt;Le prompt augmenté est le suivant :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
You are a chatbot working for Stack Labs company. Your job is to retrieve information from videos.
You have to answer users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; questions using the context given below, taken from video transcripts.
Don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t invent anything, just use the context.
If you don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t have a context, answer that you can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t find one.

You must answer in english.

Context:
{context}

Question:
{query}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Par conséquent la réponse du LLM peut comporter trois types de réponses : &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Le chatbot dit qui il est et à quoi il sert&lt;/li&gt;
&lt;li&gt;Le chatbot fournit un discours technique sur la base du contexte retrouvé dans Firestore&lt;/li&gt;
&lt;li&gt;Le chatbot dit qu'il n'a pas de contexte&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On ne veut fournir un lien cliquable à l'utilisateur que s'il s'agit d'un discours technique. On va donc faire un appel supplémentaire à Gemini 1.5 Flash, en le forçant à fournir l'un des trois flags suivants :&lt;br&gt;
     - &lt;code&gt;who_am_i&lt;/code&gt; : Le chatbot se présente.&lt;br&gt;
     - &lt;code&gt;technical_speech&lt;/code&gt; : Réponse technique.&lt;br&gt;
     - &lt;code&gt;no_context&lt;/code&gt; : Aucune information trouvée.&lt;/p&gt;

&lt;p&gt;Il est possible de configurer Gemini pour qu'il retourne une réponse au format JSON en respectant un schéma :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="n"&gt;classification_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;who_am_i&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical_speech&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Message type based on context.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le prompt donné à Gemini pour la classification est le suivant :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Message : {message}

Classify this message by categorizing it into one of the following categories:
    - who_am_i: the message is someone presenting himself
    - technical_speech: Contains technical jargon, code-related keywords, or specific terminologies.
    - no_context: the message says that no context is found
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si la réponse appartient à la catégorie &lt;code&gt;technical_speech&lt;/code&gt;, l’URL est ajoutée à la réponse.&lt;/p&gt;

&lt;p&gt;Voici un aperçu du résultat : &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funw33gy7yacj17hlfum5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Funw33gy7yacj17hlfum5.png" alt="Resultat" width="800" height="695"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Avantages de cette architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Liens contextuels précis&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Le chatbot fournit des réponses enrichies avec des liens vidéo cliquables, garantissant une navigation rapide vers l’information.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Scalabilité et efficacité&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;L’utilisation de Cloud Run et EventArc permet une exécution à la demande et une scalabilité horizontale, tout en garantissant des coûts contenus.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Modularité&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Chaque microservice est indépendant, ce qui facilite les mises à jour et l’ajout de nouvelles fonctionnalités. Chaque microservice Cloud Run peut être géré de façon indépendante : gestion de la capacité mémoire et CPU, montage d'un volume, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Personnalisation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Cette solution peut être facilement personnalisée pour différents contextes, notamment le choix de la taille et du recouvrement des différents segments audios.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Cette architecture montre comment Google Cloud peut permettre une recherche d'informations efficace dans un corpus de vidéos. Le choix d’une approche &lt;strong&gt;serverless&lt;/strong&gt; et &lt;strong&gt;event-driven&lt;/strong&gt; garantit un système flexible, scalable, facile à maintenir et peu coûteux.&lt;/p&gt;

&lt;p&gt;Merci d'avoir lu ! Nous sommes Victor et Maximilien, développeur et data engineer chez Stack Labs. Si vous souhaitez découvrir la &lt;a href="https://cloud.stack-labs.com/cloud-data-platform" rel="noopener noreferrer"&gt;Stack Labs Data Platform&lt;/a&gt; ou rejoindre &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;une équipe tech motivée&lt;/a&gt;, n'hésitez pas à nous contacter.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>rag</category>
      <category>genai</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How to pass an Array of Structs in Bigquery's parameterized queries</title>
      <dc:creator>matthieucham</dc:creator>
      <pubDate>Tue, 15 Oct 2024 06:57:39 +0000</pubDate>
      <link>https://dev.to/stack-labs/how-to-pass-an-array-of-structs-in-bigquerys-parameterized-queries-39nm</link>
      <guid>https://dev.to/stack-labs/how-to-pass-an-array-of-structs-in-bigquerys-parameterized-queries-39nm</guid>
      <description>&lt;p&gt;In Google's Bigquery, &lt;a href="https://cloud.google.com/bigquery/docs/parameterized-queries" rel="noopener noreferrer"&gt;SQL queries can be parameterized&lt;/a&gt;. If you're not familiar with this concept, it basically means that you can write SQL queries as parameterized templates like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;mydataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mytable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columnA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;valueA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;valueB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pass the values separately. This has numerous benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The query is more readable than when it's built by string concatenation&lt;/li&gt;
&lt;li&gt;The code is more robust and industrialized&lt;/li&gt;
&lt;li&gt;It's a great protection against SQL injection attacks &lt;a href="https://xkcd.com/327" rel="noopener noreferrer"&gt;(mandatory XKCD)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The passing of query parameters from a Python script appears straightforward... at first sight. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud.bigquery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ScalarQueryParameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ArrayQueryParameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StructQueryParameter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
INSERT INTO mydataset.mytable(columnA, columnB)
    VALUES (@valueA, @valueB)
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ScalarQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valueA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
        &lt;span class="nc"&gt;ScalarQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valueB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example above inserts simple ("Scalar") values in columns A and B. But you can also pass more complex parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arrays (ArrayQueryParameter)&lt;/li&gt;
&lt;li&gt;Structs (StructQueryParameter)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Problems arise when you want to insert arrays of structs : there are many gotchas, almost no documentation and very few resources on the subject on the web. The goal of this article is to fill this gap.&lt;/p&gt;

&lt;h1&gt;
  
  
  How to persist an array of structs in bigquery using parameterized queries
&lt;/h1&gt;

&lt;p&gt;Let's define the following object that we want to store in our destination table&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;capital_city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Continent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;by invoking this parameterized query&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;continents&lt;/span&gt; &lt;span class="n"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nd"&gt;@countries&lt;/span&gt; &lt;span class="n"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Oceania&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first try by following the &lt;a href="https://cloud.google.com/bigquery/docs/samples/bigquery-query-params-arrays" rel="noopener noreferrer"&gt;shallow documentation&lt;/a&gt; would be&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;job_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ArrayQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
             &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New Zealand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capital_city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Wellington&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
             &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fiji&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capital_city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Suva&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which would fail miserably&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AttributeError: 'dict' object has no attribute 'to_api_repr'&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Gotcha n°1: ArrayQueryParameter's values must be instances of StructQueryParameter
&lt;/h2&gt;

&lt;p&gt;It turns out that the third argument of the constructor - &lt;code&gt;values&lt;/code&gt;- must be a collection of &lt;code&gt;StructQueryParameter&lt;/code&gt; instances, not the wanted values directly. So let's build them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;job_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;ArrayQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;StructQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;ScalarQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
        &lt;span class="nc"&gt;ScalarQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capital_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capital_city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time it works... Until you try to set an empty array&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;job_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;ArrayQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RECORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ValueError: Missing detailed struct item type info for an empty array, please provide a StructQueryParameterType instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Gotcha n°2: Provide the full structure type as second argument
&lt;/h2&gt;

&lt;p&gt;The error message is pretty clear: "RECORD" is not enough for Bigquery to know what to do with your empty array. It needs the fully detailed structure. So be it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;QueryJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;ArrayQueryParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;StructQueryParameterType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ScalarQueryParameterType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;ScalarQueryParameterType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capital_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Notice how the order of the arguments of the &lt;code&gt;...ParameterType&lt;/code&gt; constructor is the reverse of &lt;code&gt;...Parameter&lt;/code&gt; constructor. Just another trap on the road...)&lt;/p&gt;

&lt;p&gt;And now it works for empty arrays too, yay !&lt;/p&gt;

&lt;p&gt;One last gotcha to be aware of: &lt;strong&gt;every subfield of a StructQueryParameterType must have a name&lt;/strong&gt;, even if the second parameter (&lt;code&gt;name&lt;/code&gt;) is optional in the constructor. It's actually mandatory for subfields, otherwise you'll get a new kind of error&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Empty struct field name&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think that's all we need to know to complete the usage of arrays of records in query parameters, I hope this helps !&lt;/p&gt;




&lt;p&gt;Thanks for reading! I’m Matthieu, data engineer at Stack Labs.&lt;br&gt;
If you want to discover the &lt;a href="https://cloud.stack-labs.com/cloud-data-platform" rel="noopener noreferrer"&gt;Stack Labs Data Platform&lt;/a&gt; or join an enthousiast &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;Data Engineering team&lt;/a&gt;, please contact us.&lt;/p&gt;




&lt;p&gt;Photo de &lt;a href="https://unsplash.com/fr/@dnevozhai?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Denys Nevozhai&lt;/a&gt; sur &lt;a href="https://unsplash.com/fr/photos/photographie-en-contre-plongee-de-linterieur-du-batiment-JsdvKIcvAGo?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bigquery</category>
      <category>python</category>
      <category>googlecloud</category>
      <category>sql</category>
    </item>
    <item>
      <title>Automatically Update BigQuery View Schema Changes</title>
      <dc:creator>matthieucham</dc:creator>
      <pubDate>Tue, 30 Jul 2024 15:29:59 +0000</pubDate>
      <link>https://dev.to/stack-labs/automatically-update-bigquery-view-schema-changes-27j8</link>
      <guid>https://dev.to/stack-labs/automatically-update-bigquery-view-schema-changes-27j8</guid>
      <description>&lt;p&gt;SQL views are virtual tables simplifying data access and security. They offer tailored data perspectives, protecting sensitive information. Data analysts widely use them to streamline modeling.&lt;/p&gt;

&lt;p&gt;As such, views are a crucial feature of Google Cloud's fully managed data warehouse, BigQuery. However, they have certain &lt;a href="https://cloud.google.com/bigquery/docs/views-intro" rel="noopener noreferrer"&gt;limitations&lt;/a&gt;. One of these limitations can be particularly troublesome for data analysts and end-users:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The schemas of the underlying tables are stored with the view when the view is created. If columns are added, deleted, or modified after the view is created, the view isn't automatically updated and the reported schema will remain inaccurate until the view SQL definition is changed or the view is recreated. Even though the reported schema may be inaccurate, all submitted queries produce accurate results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To see this limitation into action, create a source table with two columns&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`demo_devto.source_table`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;B&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"b"&lt;/span&gt; 
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a view above it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="nv"&gt;`demo_devto.expo_view`&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;`demo_devto.source_table`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As expected, the schema of the view presents 2 columns A and B&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi6nor5lksypwnjjpn9i1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi6nor5lksypwnjjpn9i1.png" alt="Image description" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now add a column to the source table&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`demo_devto.source_table`&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="k"&gt;C&lt;/span&gt; &lt;span class="n"&gt;STRING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new column is reflected by the source table's schema&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6vykfljuqqfjqs549ku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw6vykfljuqqfjqs549ku.png" alt="Image description" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But not by the view's schema&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n68ga6nquqf50vw5vsw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8n68ga6nquqf50vw5vsw.png" alt="Image description" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still, the result of a query is correct with 3 columns&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qg9ojd430hk2tdj294e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7qg9ojd430hk2tdj294e.png" alt="Image description" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This article outlines a method to circumvent this limitation and maintain the view's schema in alignment with the underlying table's schema as closely as possible.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A fully serverless event-driven architecture to synchronize schemas
&lt;/h2&gt;

&lt;p&gt;This solution make use of a log sink to capture audit logs from BigQuery, a PubSub topic where relevant log entries are directed, a PubSub subscription and a Cloud Run service to process them&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq51bmh4xc7w8obrea9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq51bmh4xc7w8obrea9d.png" alt="Image description" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's review each step and dive into details&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Bigquery audit logs
&lt;/h3&gt;

&lt;p&gt;All Google Cloud services generate logs which are viewable in Cloud Logging. BigQuery is no exception and audit logs offer all information we need. See their structure &lt;a href="https://cloud.google.com/bigquery/docs/reference/auditlogs" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cloud Logging log sink
&lt;/h3&gt;

&lt;p&gt;A log sink is a location where the logs are collected and stored. Google Cloud Logging log sinks collect within a scope - project, folder, organization. So to capture update logs from tables for a whole organization, a log sink at organization level is needed. To monitor a project only, a sink at project level is enough. &lt;/p&gt;

&lt;p&gt;A log sink must declare a filter. This is very important to limit costs - which depend of the volume of captured logs - and to process relevant events only. Here we are using the following filter to capture events about schema changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource.type="bigquery_resource"
AND protoPayload.serviceName="bigquery.googleapis.com"
AND protoPayload.methodName="tableservice.update"
AND protoPayload.authenticationInfo.principalEmail !~ &amp;lt;regex identifying the service account used by the cloud run service who process logs&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The filter on principalEmail serves as a mechanism to identify updates to exposition views made by the Cloud Run service, which we wish to exclude from processing as our focus lies solely on source table update events.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally, we need to give the sink a destination, where received logs who pass the filter are routed. &lt;a href="https://cloud.google.com/logging/docs/export/aggregated_sinks#supported-destinations" rel="noopener noreferrer"&gt;Several kinds of destination are possible.&lt;/a&gt; Because our architecture is event-driven, the selected destination is a PubSub topic. The log entry is then encoded as JSON&lt;/p&gt;

&lt;p&gt;Here is how to provision such a sink with Terraform, at project level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_logging_project_sink"&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-project"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"logsink-demo"&lt;/span&gt;
  &lt;span class="nx"&gt;destination&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pubsub.googleapis.com/${google_pubsub_topic.demo.id}"&lt;/span&gt;
  &lt;span class="nx"&gt;filter&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
    resource.type="bigquery_resource"
    AND protoPayload.serviceName="bigquery.googleapis.com"
    AND protoPayload.methodName="tableservice.update"
    AND protoPayload.authenticationInfo.principalEmail !~ "^sa-demo@myproject.iam.gserviceaccount.com$"
&lt;/span&gt;&lt;span class="no"&gt;  EOT
&lt;/span&gt;  &lt;span class="nx"&gt;unique_writer_identity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_pubsub_topic_iam_member"&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;topic&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_pubsub_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"roles/pubsub.publisher"&lt;/span&gt;
  &lt;span class="nx"&gt;member&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_logging_project_sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writer_identity&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. PubSub topic and subscription
&lt;/h3&gt;

&lt;p&gt;The PubSub topic is the destination of log events who pass the log sink filter.&lt;/p&gt;

&lt;p&gt;To consume these events, a subscription in PUSH mode send these events to a HTTPS endpoint.&lt;/p&gt;

&lt;p&gt;Here is an example of how these resources can be provisioned with Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_pubsub_topic"&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"topic-demo"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_pubsub_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sub-demo"&lt;/span&gt;
  &lt;span class="nx"&gt;topic&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_pubsub_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;ack_deadline_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;

  &lt;span class="nx"&gt;push_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;push_endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="nx"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;oidc_token&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;service_account_email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;expiration_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. 5. and 6. Events processing
&lt;/h3&gt;

&lt;p&gt;The processing of log events is performed by a Cloud Run service in this system, but could be done by a Cloud Function for example.&lt;/p&gt;

&lt;p&gt;In Python, the decoding of incoming events can be done like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;bq_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By parsing the bq_log object, we can retrieve the updated table id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud.bigquery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TableReference&lt;/span&gt;

&lt;span class="n"&gt;RESOURCENAME_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^projects/(?P&amp;lt;project&amp;gt;[^/]+)/datasets/(?P&amp;lt;dataset&amp;gt;[^/]+)/tables/(?P&amp;lt;table&amp;gt;[^/]+)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resource_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bq_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protoPayload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resourceName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RESOURCENAME_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;TableReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_api_repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step is to identify the views which relies on this source table. Here, associations between source tables and exposition views are registered in a Firestore database, but other designs are possible. For example, you could query INFORMATION_SCHEMA.VIEWS metadata views and identify the affected views by parsing the content of the VIEW_DEFINITION column&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VIEW_DEFINITION&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;`demo_devto.INFORMATION_SCHEMA.VIEWS`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, synchronize all affected views. BigQuery views seem to not support the updating of the "schema" field by the &lt;code&gt;update_table()&lt;/code&gt; method when columns are added. The recommended way is then to re-create views with &lt;a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/data-definition-language#create_view_statement" rel="noopener noreferrer"&gt;SQL DDL statements&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With all steps pieced together, any schema update from source tables automatically triggers the re-creation of exposition views, keeping the schema synchronized after a short delay !&lt;/p&gt;




&lt;p&gt;Thanks for reading! I’m Matthieu, data engineer at Stack Labs.&lt;br&gt;
If you want to discover the &lt;a href="https://cloud.stack-labs.com/cloud-data-platform" rel="noopener noreferrer"&gt;Stack Labs Data Platform&lt;/a&gt; or join an enthousiast &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;Data Engineering team&lt;/a&gt;, please contact us.&lt;/p&gt;




&lt;p&gt;Cover picture by &lt;a href="https://unsplash.com/fr/@migueldelmar?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Miguel Delmar&lt;/a&gt; on &lt;a href="https://unsplash.com/fr/photos/plan-deau-bleu-W4qWlYbYqI4?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bigquery</category>
      <category>eventdriven</category>
      <category>googlecloud</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Enhancing Video to Text Transcription with AI: An Asynchronous Solution on Google Cloud Platform</title>
      <dc:creator>Maximilien Soviche</dc:creator>
      <pubDate>Wed, 03 Jul 2024 09:50:46 +0000</pubDate>
      <link>https://dev.to/stack-labs/enhancing-video-to-text-transcription-with-ai-an-asynchronous-solution-on-google-cloud-platform-59el</link>
      <guid>https://dev.to/stack-labs/enhancing-video-to-text-transcription-with-ai-an-asynchronous-solution-on-google-cloud-platform-59el</guid>
      <description>&lt;p&gt;Asynchronous transcription can be applied in various contexts. For developers looking to implement a robust, scalable, and efficient transcription solution, the Google Cloud Platform (GCP) offers an ideal environment. In this article, we’ll explore an asynchronous video-to-text transcription solution built with GCP using an event-driven and serverless architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Potential Applications
&lt;/h3&gt;

&lt;p&gt;The provided solution is particularly well-suited for long video-to-text transcriptions, efficiently handling videos that are more than an hour long. This makes it ideal for a wide array of applications across various sectors. Here are some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;State Institutions or local authorities:&lt;/strong&gt; Transcribing meetings, hearings, and other official recordings to ensure transparency and accessibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Company Meetings:&lt;/strong&gt; Creating accurate records of internal meetings, conferences, and training sessions to enhance communication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Educational Institutions:&lt;/strong&gt; Transcribing lectures, seminars, and workshops to aid in learning and research.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For shorter videos, the Gemini Pro API can handle the entire video-to-text transcription process, offering a streamlined and efficient solution for quicker, smaller-scale transcription needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl77y0y5rb34xxrvyolvo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl77y0y5rb34xxrvyolvo.png" alt="Architecture overview" width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our solution comprises three event-driven Cloud Functions, each triggered by specific events in different Cloud Storage buckets using an Eventarc trigger. Event-driven Cloud Functions are deployed pieces of code on GCP, invoked in response to an event in the cloud environment. In our case, we want our functions to be invoked when a file is upload in a specific Cloud Storage bucket. Eventarc is a standardized solution to manage events on GCP. Eventarc triggers route these events between resources. In this particular case, each Eventarc trigger listen to new objects in a specific Cloud Storage bucket, and then triggers the associated Cloud Function. The event data is passed to the function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/functions/docs/writing/write-event-driven-functions#cloudevent-example-python" rel="noopener noreferrer"&gt;More information about Cloud Functions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/functions/docs/calling/eventarc" rel="noopener noreferrer"&gt;More information about Eventarc triggers&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The four buckets used in our architecture are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Video Files Bucket:&lt;/strong&gt; Where users upload their video files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio Files Bucket:&lt;/strong&gt; Stores the extracted audio files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw Transcriptions Bucket:&lt;/strong&gt; Contains the initial transcriptions generated by the Chirp speech-to-text model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curated Transcriptions Bucket:&lt;/strong&gt; Stores the curated transcriptions, enhanced by Gemini.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application architecture is designed to be modular and scalable. Here’s a step-by-step breakdown of the workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Video Upload and Audio Extraction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user uploads a video file to the &lt;code&gt;video-files&lt;/code&gt; bucket, the &lt;code&gt;video-to-audio&lt;/code&gt; Cloud Function is triggered. This function uses &lt;code&gt;ffmpeg&lt;/code&gt; to extract the audio from the video file and save it in the &lt;code&gt;audio-files&lt;/code&gt; bucket.&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions_framework&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convert_video_to_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Video to audio event-triggered cloud function.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;video_file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUDIO_FILES_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.mp4&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.mov&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.avi&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.mkv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is not a supported video format.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tmp_video_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_to_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_video_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;audio_file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.mp3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;tmp_audio_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg -i &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tmp_video_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -vn -acodec libmp3lame -q:a 2 &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tmp_audio_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shell&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination_bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_audio_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_video_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_audio_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Converted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; and uploaded to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;destination_bucket_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Audio to Text Transcription&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The upload of the audio file to the &lt;code&gt;audio-files&lt;/code&gt; bucket triggers the &lt;code&gt;audio-to-text&lt;/code&gt; Cloud Function. This function uses Chirp, a highly accurate speech-to-text model, and the Speech-to-Text API, to transcribe the audio and stores the raw transcription in the &lt;code&gt;raw-transcriptions&lt;/code&gt; bucket.&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud.speech_v2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpeechClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud.speech_v2.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cloud_speech&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.api_core.client_options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientOptions&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions_framework&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chirp_model_long&lt;/span&gt;


&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe_batch_gcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gcs_uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchRecognizeResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Transcribes audio from a Google Cloud Storage URI.

    Parameters
    ----------
        project_id: The Google Cloud project ID.
        gcs_uri: The Google Cloud Storage URI.

    Returns
    -------
        The RecognizeResponse.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpeechClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;client_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ClientOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;api_endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-speech.googleapis.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RecognitionConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;auto_decoding_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AutoDetectDecodingConfig&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;language_codes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fr-FR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chirp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;file_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BatchRecognizeFileMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gcs_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BatchRecognizeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;recognizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/locations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/recognizers/_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;file_metadata&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;recognition_output_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RecognitionOutputConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;inline_response_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InlineOutputConfig&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;processing_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cloud_speech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchRecognizeRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessingStrategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DYNAMIC_BATCHING&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch_recognize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Waiting for operation to complete...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gcs_uri&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alternatives&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcript: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alternatives&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alternatives&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Transcript: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;


&lt;span class="nd"&gt;@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;speech_to_text_transcription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Audio file transcription via Speech-to-text API call.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;

    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;audio_file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAW_TRANSCRIPTIONS_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transcribe_batch_gcs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;gcs_uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gs://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;raw_transcription_file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio_file_name&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_raw.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination_bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_transcription_file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB DONE IN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SECONDS.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;About Chirp:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Chirp is a state-of-the-art speech-to-text model developed to provide highly accurate and fast transcription services. It supports a wide range of languages and dialects, making it a versatile choice for diverse transcription needs. It is available in the Speech-to-Text API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/speech-to-text/v2/docs/batch-recognize" rel="noopener noreferrer"&gt;More information about long audio to text transcription&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transcription Curation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, the &lt;code&gt;curate-transcription&lt;/code&gt; Cloud Function is triggered by the new transcription file in the &lt;code&gt;raw-transcriptions&lt;/code&gt; bucket. This function sends the raw transcription to the Gemini API that uses &lt;code&gt;gemini-pro&lt;/code&gt; model for curation and stores the refined transcription in the &lt;code&gt;curated-transcriptions&lt;/code&gt; bucket.&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;vertexai&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vertexai.generative_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GenerativeModel&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions_framework&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@functions_framework.cloud_event&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcription_correction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Gemini API call to correct and enhance speech-to-text transcription.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;

    &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;raw_transcription_filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CURATED_TRANSCRIPTIONS_BUCKET_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event type: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw_transcription_filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;storage_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_transcription_filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;download_as_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;vertexai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-central1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerativeModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-1.0-pro-002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        YOUR CUSTOM PROMPT GOES HERE. 
        PROVIDING CONTEXT AND GIVING INFORMATION ABOUT THE RESULT YOU EXPECT IS NECESSARY.

        &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;n_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB : SPEECH-TO-TEXT TRANSCRIPTION CORRECTION. &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_billable_characters&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; BILLABLE CHARACTERS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RESPONSE WILL PROCESS &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; TOKENS.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;curated_filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_transcription_filename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_curated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;destination_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination_bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curated_filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;destination_blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB DONE IN &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; SECONDS.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chosen architecture is modular and event-driven, which brings several advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; This application can handle short or long videos, up to 8 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility:&lt;/strong&gt; The separation of concerns allows for easy maintenance and upgrades. If the user uploads a video in the &lt;code&gt;video-files&lt;/code&gt; bucket, the three cloud function will be triggered. But if the user upload an audio in the &lt;code&gt;audio-files&lt;/code&gt; bucket, then only the two last cloud functions will be triggered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-Efficiency:&lt;/strong&gt; Cloud Functions are serverless. Using Cloud Functions ensures that resources are only used when necessary, reducing costs.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deployment with Terraform
&lt;/h3&gt;

&lt;p&gt;To ensure our solution is not only powerful but also easily manageable and deployable, we use Terraform for infrastructure as code (IaC). Terraform allows us to define our cloud resources in declarative configuration files, providing several key benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure configurations can be version-controlled using Git, following GitOps principles. This means changes to the infrastructure are tracked, reviewed, and can be rolled back if necessary.&lt;/li&gt;
&lt;li&gt;As our application grows, Terraform makes it easy to manage our infrastructure by simply updating the configuration files.&lt;/li&gt;
&lt;li&gt;Terraform makes the deployment of our application reliable and repeatable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this particular case, four cloud storage buckets and three cloud functions are needed. We use one terraform resource for the cloud functions and another for the buckets. This provides a flexible code, and makes it easier to integrate and manage new buckets or cloud functions. More information about terraform : &lt;a href="https://developer.hashicorp.com/terraform?product_intent=terraform" rel="noopener noreferrer"&gt;Terraform documentation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# locals.tf
&lt;/span&gt;
&lt;span class="nb"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;function_definitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;convert_video_to_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../services/video_to_audio_cloud_function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_files_bucket_name&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;speech_to_text_transcription&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../services/transcript_cloud_function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio_files_bucket_name&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcription_correction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;source_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../services/gemini_cloud_function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_transcriptions_bucket_name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this &lt;code&gt;locals.tf&lt;/code&gt; file, the user can add, configure or remove cloud functions very easily. The &lt;code&gt;cloud_functions.tf&lt;/code&gt; file uses one terraform resource for all cloud functions, and loops over these function definitions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cloud_functions.tf
&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;random_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;byte_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_storage_bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_code_bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${random_id.bucket_prefix.hex}-source-code-bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt;                    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="n"&gt;force_destroy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;uniform_bucket_level_access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archive_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;for_each&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;in&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_definitions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;}
  &lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/${each.value.name}-source.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;source_dir&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_dir&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_storage_bucket_object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_sources&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${basename(each.value.output_path)}-${each.value.output_md5}.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;bucket&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_storage_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_code_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_storage_project_service_account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_project_iam_member&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gcs_pubsub_publishing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy_project&lt;/span&gt;
  &lt;span class="n"&gt;role&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/pubsub.publisher&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;member&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serviceAccount:${data.google_storage_project_service_account.default.email_address}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_service_account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;account_id&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gcf-sa&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test Service Account - used for both the cloud function and eventarc trigger in the test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_project_iam_member&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/run.invoker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_receiving&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/eventarc.eventReceiver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactregistry_reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/artifactregistry.reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage_object_admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/storage.objectUser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;speech_client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;              &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/speech.client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insights_collector_service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/storage.insightsCollectorService&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aiplatform_user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles/aiplatform.user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;project&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy_project&lt;/span&gt;
  &lt;span class="n"&gt;role&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="n"&gt;member&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serviceAccount:${google_service_account.account.email}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;google_project_iam_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gcs_pubsub_publishing&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_cloudfunctions2_function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;functions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;in&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_definitions&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="err"&gt;}
  &lt;/span&gt;&lt;span class="nf"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;google_project_iam_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_receiving&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;google_project_iam_member&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;artifactregistry_reader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Function to process ${each.value.name}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

  &lt;span class="n"&gt;build_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;runtime&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python39&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;entry_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="n"&gt;environment_variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;BUILD_CONFIG_TEST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;build_test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;storage_source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_storage_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_code_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_storage_bucket_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;service_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;min_instance_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;max_instance_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="n"&gt;available_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;timeout_seconds&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="n"&gt;available_cpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
    &lt;span class="n"&gt;environment_variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;                         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy_project&lt;/span&gt;
      &lt;span class="n"&gt;AUDIO_FILES_BUCKET_NAME&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio_files_bucket_name&lt;/span&gt;
      &lt;span class="n"&gt;RAW_TRANSCRIPTIONS_BUCKET_NAME&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_transcriptions_bucket_name&lt;/span&gt;
      &lt;span class="n"&gt;CURATED_TRANSCRIPTIONS_BUCKET_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;curated_transcriptions_bucket_name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ingress_settings&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALLOW_INTERNAL_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;all_traffic_on_latest_revision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;service_account_email&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;event_trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;trigger_region&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt;            &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google.cloud.storage.object.v1.finalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;retry_policy&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRY_POLICY_RETRY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;service_account_email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;event_filters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;google_storage_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_transcription_bucket_set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly the &lt;code&gt;buckets.tf&lt;/code&gt; file uses only one terraform resource for all Cloud Storage buckets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# buckets.tf
&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google_storage_bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video_transcription_bucket_set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_files_bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audio_files_bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_transcriptions_bucket_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;curated_transcriptions_bucket_name&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
  &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;
  &lt;span class="n"&gt;storage_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STANDARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;force_destroy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;uniform_bucket_level_access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Costs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Storage:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
$0.026 per gigabyte per month&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speech-to-Text API v2:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Depends on the amount of audio you plan to process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;$0.016 per minute processed per month for 0 to 500,000 minutes of audio&lt;/li&gt;
&lt;li&gt;$0.01 per minute processed per month for 500,000 to 1,000,000 minutes of audio&lt;/li&gt;
&lt;li&gt;$0.008 per minute processed per month for 1,000,000 to 2,000,000 minutes of audio&lt;/li&gt;
&lt;li&gt;$0.004 per minute processed per month for over 2,000,000 minutes of audio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://cloud.google.com/speech-to-text/pricing/?gad_source=1&amp;amp;gclid=Cj0KCQjw7ZO0BhDYARIsAFttkCiLzcwnuxXb6_NolGidjKV6bKwQ8DKOYA17MNfWi3Oj8jBIToaoD_saAsbBEALw_wcB&amp;amp;gclsrc=aw.ds&amp;amp;hl=en" rel="noopener noreferrer"&gt;Pricing details&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini API:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Under the following limits, the service is free of charge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 requests per minute&lt;/li&gt;
&lt;li&gt;1 million tokens per minute&lt;/li&gt;
&lt;li&gt;1,500 requests per day
If you want to exceed these limits, a pay-as-you-go policy is applied. &lt;a href="https://ai.google.dev/pricing" rel="noopener noreferrer"&gt;Pricing details&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cloud Functions:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The pricing depends on how long the function runs, how many times it is triggered, and the resources that are provisioned. &lt;a href="https://cloud.google.com/functions/pricing#simple_event-driven_function" rel="noopener noreferrer"&gt;The following link explains the pricing policy for event-driven cloud functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Estimate the costs of your solution with &lt;a href="https://cloud.google.com/products/calculator?hl=en" rel="noopener noreferrer"&gt;Google Cloud’s pricing calculator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
A state institution wants to automate transcript generation for meetings. The average duration of these meetings is 4 hours. The records are uploaded to GCP using this solution. Let’s simulate the costs of one transcription for this specific use case using the simulator:&lt;/p&gt;

&lt;p&gt;The final cost per month, with one transcription per month, is estimated to be $5.14. More than half the costs are due to Speech-to-Text API use.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service Display Name&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;Total Price (USD)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Speech-to-Text V2&lt;/td&gt;
&lt;td&gt;Cloud Speech-to-Text Recognition&lt;/td&gt;
&lt;td&gt;240.0&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;3.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 1&lt;/td&gt;
&lt;td&gt;CPU Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;40080000&lt;/td&gt;
&lt;td&gt;us-central1&lt;/td&gt;
&lt;td&gt;0.96192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 1&lt;/td&gt;
&lt;td&gt;Memory Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;25600000000&lt;/td&gt;
&lt;td&gt;us-central1&lt;/td&gt;
&lt;td&gt;0.0625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 1&lt;/td&gt;
&lt;td&gt;Invocations (2nd Gen)&lt;/td&gt;
&lt;td&gt;1000.0&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 2&lt;/td&gt;
&lt;td&gt;CPU Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;4008000.0&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.09619&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 2&lt;/td&gt;
&lt;td&gt;Memory Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;2560000000&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.00625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 2&lt;/td&gt;
&lt;td&gt;Invocations (2nd Gen)&lt;/td&gt;
&lt;td&gt;1000.0&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 3&lt;/td&gt;
&lt;td&gt;CPU Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;4008000.0&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.09619&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 3&lt;/td&gt;
&lt;td&gt;Memory Allocation Time (2nd Gen)&lt;/td&gt;
&lt;td&gt;2560000000&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.00625&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Functions 3&lt;/td&gt;
&lt;td&gt;Invocations (2nd Gen)&lt;/td&gt;
&lt;td&gt;1000.0&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage 1&lt;/td&gt;
&lt;td&gt;Standard Storage Belgium&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage 2&lt;/td&gt;
&lt;td&gt;Standard Storage Belgium&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage 3&lt;/td&gt;
&lt;td&gt;Standard Storage Belgium&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.0002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Storage 4&lt;/td&gt;
&lt;td&gt;Standard Storage Belgium&lt;/td&gt;
&lt;td&gt;0.01&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;0.0002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Total Price:&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.1397&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prices are in US dollars, effective date is 2024-07-01T08:32:56.935Z&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The estimated fees provided by Google Cloud Pricing Calculator are for discussion purposes only and are not binding on either you or Google. Your actual fees may be higher or lower than the estimate.&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Url to the estimate:&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cloud.google.com/calculator?dl=CiRkY2ZmMWNiOC1hNjY5LTQ4M2YtYTZlNi1mYjgzMWNkYmNlMzYaJEUyQUE4MUM5LTA1RTMtNEIzRS1BNEJGLUY4QzU3NTQ3MzVCMg==" rel="noopener noreferrer"&gt;Link to estimate&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Leveraging AI to enhance video-to-text transcription on Google Cloud Platform offers significant benefits in scalability, flexibility, and efficiency. By integrating Chirp for speech-to-text conversion and Gemini Pro for transcription curation, and managing the deployment with Terraform, this solution provides a robust, easily deployable framework for high-quality transcriptions across various applications.&lt;/p&gt;

&lt;p&gt;Thanks for reading! I’m Maximilien, data engineer at Stack Labs.&lt;br&gt;
If you want to discover the &lt;a href="https://cloud.stack-labs.com/cloud-data-platform" rel="noopener noreferrer"&gt;Stack Labs Data Platform&lt;/a&gt; or join &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;an enthousiast Data Engineering team&lt;/a&gt;, please contact us.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>python</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>Montrez patte blanche : tuez des mutants !</title>
      <dc:creator>Antoine Aubé</dc:creator>
      <pubDate>Thu, 16 May 2024 11:52:08 +0000</pubDate>
      <link>https://dev.to/stack-labs/montrez-patte-blanche-tuez-des-mutants--12el</link>
      <guid>https://dev.to/stack-labs/montrez-patte-blanche-tuez-des-mutants--12el</guid>
      <description>&lt;p&gt;Faites-vous confiance à vos logiciels préférés ? Probablement. Pourquoi leur faites-vous confiance ? L'ont-ils mérité ? La pléthore d'outils que nous utilisons au quotidien nous est souvent imposée par les mêmes personnes qui attendent de notre travail une haute qualité. Pourtant, dans le feu de l'action, rares sont ceux qui questionnent la qualité des ces outils là.&lt;/p&gt;

&lt;p&gt;Le test logiciel est plébiscité dans l'industrie pour montrer patte blanche. Compte tenu des contraintes du monde réel (temps, argent), une question se pose : quels tests dois-je écrire ? Ou formulé autrement, dès lors que nous avons la notion de &lt;em&gt;bon&lt;/em&gt; test : quand dois-je arrêter d'en écrire ? Pour nous aider à y répondre, nous utilisons souvent la couverture de code par les tests dans l'industrie. Dans cet article, je présente une suggestion de complément à cette mesure, une pratique : le test de mutation.&lt;/p&gt;

&lt;p&gt;Dans un premier temps, nous reviendrons sur les concepts évoqués ci-dessus, en nous demandant à quoi ils peuvent nous servir. Dans un second temps, nous nous concentrerons sur l'écosystème Go : d'abord en passant en revue les outils qui automatisent le test de mutation, puis en utilisant ces outils pour savoir si nous avons raison de nous fier à nos logiciels (écrits en Go) préférés.&lt;/p&gt;

&lt;h2&gt;
  
  
  D'une spécification à la confiance dans son implémentation logicielle
&lt;/h2&gt;

&lt;p&gt;Un programmeur programme pour satisfaire un besoin. L'expression de ce besoin est souvent laconique et informel : il faut le préciser. L'élicitation des exigences doit permettre d'en lever les zones d'ombres, et l'analyse de ces exigences d'aboutir à la spécification d'un logiciel. Cette spécification est la base sur laquelle le programmeur repose pour écrire le code source du logiciel.&lt;/p&gt;

&lt;p&gt;Prenons un exemple : la première étape du &lt;a href="https://codingdojo.org/fr/kata/FizzBuzz/" rel="noopener noreferrer"&gt;kata « &lt;em&gt;FizzBuzz&lt;/em&gt; »&lt;/a&gt;. Nous pouvons la reformuler comme suit :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Écrire une fonction qui prend, en entrée, un entier, et qui retourne « &lt;em&gt;fizz&lt;/em&gt; » si l'entier est multiple de trois, « &lt;em&gt;buzz&lt;/em&gt; » s'il est multiple de cinq, « &lt;em&gt;fizzbuzz&lt;/em&gt; » s'il est multiple à la fois de trois et de cinq, et simplement l'entier dans les autres cas.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Je me suis prêté à l'exercice en programmant une implémentation de cet énoncé en Go, une fonction nommée &lt;code&gt;MyFizzBuzz&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// (module : git.sr.ht/~arjca/fizzbuzz ; fichier : fizzbuzz.go)&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"strconv"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;MyFizzBuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"fizzbuzz"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"fizz"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"buzz"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Itoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;À ce stade, nous pouvons nous demander si l'énoncé est correctement implémenté. Autrement dit : si j'utilise &lt;code&gt;MyFizzBuzz&lt;/code&gt;, se comportera-t-elle comme prévu ?&lt;/p&gt;

&lt;p&gt;Répondre à ce genre de questions est crucial dans l'industrie. En effet, le dysfonctionnement d'un logiciel peut coûter très cher aux organisations qui les développent, et avoir des conséquences dramatiques pour celles qui les utilisent. Concentrons-nous donc sur ce sujet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vérifier que la spéc. est correctement implémentée
&lt;/h3&gt;

&lt;p&gt;Comment s'assurer qu'un logiciel se comporte comme prévu ? Comme dit dans l'introduction, la pratique courante dans l'industrie du numérique est de tester : vérifier que le logiciel se comporte conformément aux attentes dans une situation précise.&lt;/p&gt;

&lt;p&gt;Écrivons un test pour &lt;code&gt;MyFizzBuzz&lt;/code&gt;. Quand l'entier en entrée égale trois, la consigne dit que &lt;code&gt;MyFizzBuzz&lt;/code&gt; devrait produire « &lt;em&gt;fizz&lt;/em&gt; ». Ce test est automatisé par le code suivant :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// (fichier : fizzbuzz_test.go)&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz_test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"git.sr.ht/~arjca/fizzbuzz"&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestMyFizzBuzz_3ShouldReturnFizz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fizz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MyFizzBuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exécutons le :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;ok      git.sr.ht/~arjca/fizzbuzz   0.007s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le test est réussi. Il y a au moins &lt;em&gt;un&lt;/em&gt; cas dans lequel &lt;code&gt;MyFizzBuzz&lt;/code&gt; se comporte correctement. Cela signifie-t-il que le logiciel est dépourvu de bogue ? Certes non, comme &lt;a href="http://homepages.cs.ncl.ac.uk/brian.randell/NATO/nato1969.PDF" rel="noopener noreferrer"&gt;l'a dit Dijkstra&lt;/a&gt; :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tester un programme peut démontrer la présence d'un bogue, jamais son absence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pour dissiper tous soupçons sur &lt;code&gt;MyFizzBuzz&lt;/code&gt;, il nous resterait bien du travail : 18 446 744 073 709 551 615 tests doivent encore être écrits (car les &lt;code&gt;int&lt;/code&gt; occupent 64 bits sur ma machine).&lt;br&gt;
Écrire autant de tests n'est évidemment pas raisonnable ; nous allons devoir en écrire moins, et les &lt;em&gt;bons&lt;/em&gt; tests. Mais qu'est-ce qu'un &lt;em&gt;bon&lt;/em&gt; test ? Pas simple comme question, et apparemment pas prioritaire car une autre s'impose : quand pouvons nous nous arrêter d'en écrire ? Une réponse naïve pourrait être : « &lt;em&gt;Quand nous aurons suffisamment confiance dans le logiciel !&lt;/em&gt; ». Reste à déterminer les mesures sur lesquelles fonder cette confiance.&lt;/p&gt;
&lt;h3&gt;
  
  
  Qu'ai-je déjà vérifié ?
&lt;/h3&gt;

&lt;p&gt;Une première piste, largement suivie dans l'industrie, est de mesurer la couverture de code par les tests. Communément, il s'agit du pourcentage de lignes du code source parcourues lors de l'exécution des tests (alternativement, nous pourrions compter les fonctions, les modules, ...).&lt;/p&gt;

&lt;p&gt;Calculons cette couverture pour &lt;code&gt;MyFizzBuzz&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-cover&lt;/span&gt;
&lt;span class="go"&gt;ok      git.sr.ht/~arjca/fizzbuzz   0.002s  coverage: 57.1% of statements
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour l'instant, 57.1% des lignes de &lt;code&gt;MyFizzBuzz&lt;/code&gt; sont parcourues en exécutant le test. Est-ce assez ?&lt;/p&gt;

&lt;p&gt;Il n'y a pas de réponse consensuelle à cette question. Dans l'industrie, il n'est pas rare de trouver des objectifs pour la couverture de code par les tests. 80% est un nombre récurrent, aussi bien dans les retours d'expérience de collègues que dans ma propre expérience professionnelle. Cela veut quand même dire qu'il y a 20% du code source sans le moindre contrôle.&lt;br&gt;
&lt;a href="https://stackoverflow.com/questions/90002/what-is-a-reasonable-code-coverage-for-unit-tests-and-why" rel="noopener noreferrer"&gt;Dans des échanges en ligne&lt;/a&gt;, nous pouvons trouver d'autres sons de cloche. Certains disent que 99% ou 100% sont des objectifs souhaitables, tandis que d'autres refusent tout minimum pour cette métrique.&lt;/p&gt;

&lt;p&gt;Une raison fréquemment invoquée pour refuser un minimum de couverture à atteindre, ou pour réduire ce minimum, est que cela mène à un surcoût (car cela augmente le nombre de tâches de développement : soit un surcoût financier car il faut plus de programmeurs, soit un surcoût temporel incompatible avec le respect des dates de livraison). À mon étonnement, j'ai rarement vu mentionné en ligne le surcoût lié à un dysfonctionnement non détecté lors du développement, qui justifie la démarche de test.&lt;/p&gt;

&lt;p&gt;S'il y a un seuil à atteindre, peu importe lequel dans ce que nous avons mentionné plus haut, &lt;code&gt;MyFizzBuzz&lt;/code&gt; n'est pas à la hauteur. Améliorons ce score avec un nouveau test :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// (fichier : fizzbuzz_test.go)&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz_test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"git.sr.ht/~arjca/fizzbuzz"&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestMyFizzBuzz_3ShouldReturnFizz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"fizz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MyFizzBuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestMyFizzBuzz_yolo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fizzbuzz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MyFizzBuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;À présent, recalculons :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-cover&lt;/span&gt;
&lt;span class="go"&gt;ok      git.sr.ht/~arjca/fizzbuzz   0.002s  coverage: 100.0% of statements
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nous voici à présent à 100% de couverture ! Peu importe l'objectif, il est forcément atteint. Hélas, nous avons un peu triché : le nouveau test ne vérifie pas les valeurs produites par &lt;code&gt;MyFizzBuzz&lt;/code&gt;... Qu'avons-nous donc montré ? Tout au plus, qu'il n'y a pas de code mort dans &lt;code&gt;MyFizzBuzz&lt;/code&gt; : nous avons pu parcourir toutes les lignes de la fonction. Mais ce n'est pas ça que nous voulions !&lt;/p&gt;

&lt;p&gt;En définitive, la couverture de code par les tests n'est pas un indicateur suffisant de la correction d'un logiciel. Il nous faut la compléter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mutez les tous !!! La spéc. reconnaîtra les siens !
&lt;/h3&gt;

&lt;p&gt;Voici une autre piste à explorer. Nous avons un plan de test qui valide un code source, certes ; mais que pouvons-nous conclure s'il valide aussi un autre code source ? Assurément, cela soulèverait des doutes quant à sa qualité, nous aurions raison de nous demander ce que ce plan de test valide réellement. Idéalement, comme nous avons pris des décisions pour programmer le logiciel &lt;em&gt;comme ça&lt;/em&gt; et pas autrement, le plan de test devrait valider &lt;em&gt;ce code source là&lt;/em&gt; et pas un autre.&lt;/p&gt;

&lt;p&gt;Précédemment, nous avons écrit un test, &lt;code&gt;TestMyFizzBuzz_3ShouldReturnFizz&lt;/code&gt;, en toute bonne foi en reposant sur la consigne. Tentons de le faire échouer en modifiant légèrement &lt;code&gt;MyFizzBuzz&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-   if n%3 == 0 {
&lt;/span&gt;&lt;span class="gi"&gt;+   if n%3 != 0 {
&lt;/span&gt;        return "fizz"
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exécutons les tests :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-cover&lt;/span&gt;
&lt;span class="go"&gt;--- FAIL: TestMyFizzBuzz_3ShouldReturnFizz (0.00s)
    fizzbuzz_test.go:12: 
            Error Trace:    /home/arjca/Projets/fizzbuzz/fizzbuzz_test.go:12
            Error:          Not equal: 
                        expected: "fizz"
                        actual  : "3"

                        Diff:
                        --- Expected
                        +++ Actual
                        @@ -1 +1 @@
                        -fizz
                        +3
            Test:           TestMyFizzBuzz_3ShouldReturnFizz
FAIL
FAIL    git.sr.ht/~arjca/fizzbuzz   0.003s
FAIL
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comme prévu, le test échoue. Nous avons bien constaté que &lt;code&gt;TestMyFizzBuzz_3ShouldReturnFizz&lt;/code&gt; aboutit à un succès avec &lt;code&gt;MyFizzBuzz&lt;/code&gt; mais pas avec une de ses variantes. Nous venons de réaliser un test de mutation :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Le plan de test valide le code source de &lt;code&gt;MyFizzBuzz&lt;/code&gt; ;&lt;/li&gt;
&lt;li&gt;Nous altérons ce code source : nous générons un &lt;em&gt;mutant&lt;/em&gt;. Une règle permettant de générer un mutant s'appelle un &lt;em&gt;mutateur&lt;/em&gt;. Par exemple, un mutateur peut demander la modification d'un &lt;code&gt;&amp;lt;&lt;/code&gt; en un &lt;code&gt;&amp;lt;=&lt;/code&gt;, ou bien changer &lt;code&gt;true&lt;/code&gt; en &lt;code&gt;false&lt;/code&gt;. Notons que les mutateurs peuvent générer des mutants identiques sémantiquement au code source d'origine : c'est un &lt;em&gt;mutant équivalent&lt;/em&gt; ;&lt;/li&gt;
&lt;li&gt;Nous confrontons le plan de test au mutant. Si au moins un test échoue, bingo : nous avons tué le mutant. Autrement, le mutant survit. Naturellement, nous voulons tuer le plus de mutants que possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expérimentons une seconde mutation :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-   if n%5 == 0 {
&lt;/span&gt;&lt;span class="gi"&gt;+   if n%5 != 0 {
&lt;/span&gt;        return "buzz"
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exécutons le plan de test. Nous constatons qu'aucun test n'échoue :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="go"&gt;ok      git.sr.ht/~arjca/fizzbuzz   0.002s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le mutant a survécu. Si un mutant survit, alors la ligne contenant la mutation est faiblement testée : il manque peut-être un test (p. ex. si la couverture de code par les tests n'est pas élevée), ou alors les tests existants ne sont pas de bonne facture.&lt;/p&gt;

&lt;p&gt;Un plan de test par mutation peut générer un très grand nombre de mutants, et nous pouvons en tirer une mesure : le score de mutation. Il est le ratio du nombre de mutants tués sur le nombre total de mutants non-équivalents. Plus le score de mutation est élevé, plus le plan de test rejette ce qui n'est pas le code source d'origine ; autrement dit, plus il rejette les tests bidons.&lt;/p&gt;

&lt;p&gt;Néanmoins, il nous faut évoquer deux difficultés :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Détecter un mutant équivalent n'est pas trivial ;&lt;/li&gt;
&lt;li&gt;Le nombre de mutants peut être très, très grand, et demander un nombre déraisonnable de manipulations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pour surmonter la première difficulté, diverses approches existent : par exemple, pour détecter les mutants équivalents, ou simplement pour ne pas les générer. &lt;a href="https://ieeexplore.ieee.org/abstract/document/6613487" rel="noopener noreferrer"&gt;Une revue de ces approches a été réalisée par Madeyski et coll. en 2017&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Pour surmonter la seconde difficulté, nous pouvons déjà nous demander comment éviter les mutants inutiles : ceux qui sont équivalents à un autre mutant. Afin d'y parvenir, &lt;a href="https://dl.acm.org/doi/abs/10.1145/3136040.3136053" rel="noopener noreferrer"&gt;Fernandes et coll. proposent un ensemble de règles pour leur génération&lt;/a&gt;. Si cela diminue le nombre de mutants à générer et à essayer de tuer, cela demande encore énormément de calculs : il nous faut les automatiser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatiser le test de mutation en Go
&lt;/h2&gt;

&lt;p&gt;La suite de cet article est consacrée au test de mutation avec le langage Go. Il existe plusieurs outils pour automatiser le test de mutation en Go ; je les passe en revue dans cette section.&lt;/p&gt;

&lt;h3&gt;
  
  
  manbearpig
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/darkhelmet/manbearpig" rel="noopener noreferrer"&gt;&lt;em&gt;manbearpig&lt;/em&gt;&lt;/a&gt; est un outil développé par Daniel Huckstep en 2013. Il va à l'essentiel : l'utilisateur spécifie un paquetage et un mutateur, puis l'outil génère les mutants et les confronte aux tests.&lt;/p&gt;

&lt;p&gt;Par exemple avec &lt;code&gt;MyFizzBuzz&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;manbearpig &lt;span class="nt"&gt;-import&lt;/span&gt; git.sr.ht/~arjca/fizzbuzz &lt;span class="nt"&gt;-mutation&lt;/span&gt; &lt;span class="s2"&gt;"=="&lt;/span&gt;
&lt;span class="go"&gt;2024/04/13 18:54:46 mutating in /tmp/manbearpig1715124091
2024/04/13 18:54:46 found 3 occurrence(s) of == in fizzbuzz.go
2024/04/13 18:54:46 mutating == to !=
2024/04/13 18:54:46 mutation 1 broke the tests properly
2024/04/13 18:54:46 mutation 2 broke the tests properly
2024/04/13 18:54:46 mutation 3 failed to break any tests
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L'outil indique seulement le nombre de mutants générés ; nous manquons de détails quant à la ligne concernée par chaque mutation, et les tests qui ont tué chaque mutant. Par conséquent, il est difficile d'analyser les résultats et d'en tirer grand chose.&lt;br&gt;
Le score de mutation peut être déduit des traces ; cela reste très manuel, d'autant plus qu'il est nécessaire d'utiliser plusieurs fois l'outil pour couvrir toutes les mutations qui nous intéressent.&lt;/p&gt;
&lt;h3&gt;
  
  
  mutator
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kisielk/mutator/tree/master" rel="noopener noreferrer"&gt;&lt;em&gt;mutator&lt;/em&gt;&lt;/a&gt; est un outil développé par Kamil Kisiel en 2013. Lui aussi va à l'essentiel, avec seulement un paquetage à fournir, et éventuellement des mutateurs.&lt;/p&gt;

&lt;p&gt;Par exemple avec &lt;code&gt;MyFizzBuzz&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mutator git.sr.ht/~arjca/fizzbuzz
&lt;span class="go"&gt;using /tmp/mutate4042507686 as a temporary directory
fizzbuzz.go has 3 mutation sites
mutation fizzbuzz.go:7:10 tests failed as expected
mutation fizzbuzz.go:11:9 tests failed as expected
mutation fizzbuzz.go:15:9 did not fail tests
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;D'une certaine manière, cet outil est complémentaire à &lt;em&gt;manbearpig&lt;/em&gt; : ici, nous savons quelle ligne accueille chaque mutation, mais pas quel mutateur est utilisé...&lt;br&gt;
Une seule utilisation est requise pour calculer le score de mutation, mais ce calcul demeure manuel.&lt;/p&gt;
&lt;h3&gt;
  
  
  ooze
&lt;/h3&gt;

&lt;p&gt;Contrairement aux projets précédents, qui sont des outils utilisables via une interface en ligne de commande, &lt;a href="https://github.com/gtramontina/ooze" rel="noopener noreferrer"&gt;&lt;em&gt;ooze&lt;/em&gt;&lt;/a&gt; est une bibliothèque. Elle est développée principalement par Guilherme Tramontina et continue de recevoir des mises à jour.&lt;/p&gt;

&lt;p&gt;Pour l'utiliser, j'ai ajouté un nouveau fichier dans mon module :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;//go:build mutation&lt;/span&gt;
&lt;span class="c"&gt;// (fichier : mutation_test.go)&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;fizzbuzz_test&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/gtramontina/ooze"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestMutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ooze&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Il ne reste plus qu'à l'exécuter :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mutation 
&lt;span class="go"&gt;┃ Releasing Ooze…
[...]
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ • Total:       18                    ┃
┃ • Killed:       7                    ┃
┃ • Survived:    11                    ┃
┠┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨
┃ ⨯ Score:     0.39 (minimum: 1.00)    ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
--- FAIL: TestMutation (5.02s)
FAIL
exit status 1
FAIL    git.sr.ht/~arjca/fizzbuzz   5.021s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nous avons ici bien plus de détails ! Les lignes que j'ai omises par souci de lisibilité documentent les mutants survivants. Le récapitulatif à la fin contient le score de mutation : 39% pour &lt;code&gt;MyFizzBuzz&lt;/code&gt;. Pas fameux !&lt;/p&gt;

&lt;p&gt;Notons que la bibliothèque permet d'implémenter de nouveaux mutateurs (appelés &lt;em&gt;virus&lt;/em&gt; dans &lt;em&gt;ooze&lt;/em&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  go-mutesting
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/avito-tech/go-mutesting" rel="noopener noreferrer"&gt;&lt;em&gt;go-mutesting&lt;/em&gt;&lt;/a&gt; est un outil développé par trois personnes de l'entreprise russe Avito.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go-mutesting &lt;span class="nb"&gt;.&lt;/span&gt;     
&lt;span class="go"&gt;[...]
FAIL "/tmp/go-mutesting-619693096/fizzbuzz.go.17" with checksum 25620396c64f05efbecca57ef98b046e
The mutation score is 0.333333 (6 passed, 12 failed, 0 duplicated, 0 skipped, total is 18)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tout comme &lt;em&gt;ooze&lt;/em&gt;, cet outil donne des détails sur les mutants survivants (que j'ai ici omis pour ne pas polluer l'espace !). Il calcule également le score de mutation : 33%. Ce n'est pas le même que celui de &lt;em&gt;ooze&lt;/em&gt; car ils n'utilisent pas les mêmes mutateurs, mais cela reste un score médiocre.&lt;/p&gt;

&lt;p&gt;Avec cet outil, il est également possible de définir de nouveaux mutateurs en implémentant une interface, mais il faut que cela soit enregistré dans le code source de l'outil ; il faut donc soit y contribuer, soit cloner ce projet.&lt;/p&gt;

&lt;h3&gt;
  
  
  gremlins
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/go-gremlins/gremlins" rel="noopener noreferrer"&gt;&lt;em&gt;gremlins&lt;/em&gt;&lt;/a&gt; est un outil initié par Davide Petilli et aux nombreux contributeurs.&lt;/p&gt;

&lt;p&gt;Il dispose d'options pour limiter le nombre de mutants évalués, et en particulier il se base sur la couverture de code par les tests : si une ligne n'est pas couverte par un test, alors il n'y a pas de raison de générer un mutant pour savoir si elle est correctement testée. Cela peut avoir des effets de bord non désirés. Par exemple, l'outil intégré à Go pour générer la couverture de code n'indique pas si les &lt;code&gt;case ...&lt;/code&gt; des &lt;code&gt;switch&lt;/code&gt; sont couverts ou non, ils ne sont juste pas suivis ; par conséquent, une mutation qui devrait être réalisée dans ces lignes ne sera pas générée par &lt;em&gt;gremlins&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Pour &lt;code&gt;MyFizzBuzz&lt;/code&gt;, cela donne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gremlins unleash       
&lt;span class="go"&gt;Starting...
Gathering coverage... done in 344.804964ms
      KILLED CONDITIONALS_NEGATION at fizzbuzz.go:11:9
       LIVED ARITHMETIC_BASE at fizzbuzz.go:15:6
       LIVED CONDITIONALS_NEGATION at fizzbuzz.go:15:9
       LIVED ARITHMETIC_BASE at fizzbuzz.go:7:6
      KILLED ARITHMETIC_BASE at fizzbuzz.go:11:6
      KILLED CONDITIONALS_NEGATION at fizzbuzz.go:7:10

Mutation testing completed in 322 milliseconds 805 microseconds
Killed: 3, Lived: 3, Not covered: 0
Timed out: 0, Not viable: 0, Skipped: 0
Test efficacy: 50.00%
Mutator coverage: 100.00%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le score de mutation,appelé ici « &lt;em&gt;efficacité des tests&lt;/em&gt; », égale 50% ; encore un autre score !&lt;/p&gt;

&lt;p&gt;L'exemple de &lt;code&gt;MyFizzBuzz&lt;/code&gt; est un peu court pour mettre en lumière les fonctionnalités de &lt;em&gt;gremlins&lt;/em&gt;. Nous pouvons noter :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilisation de plusieurs cœurs &lt;em&gt;CPU&lt;/em&gt; pour générer et évaluer plusieurs mutants à la fois ;&lt;/li&gt;
&lt;li&gt;Possibilité de générer des mutants uniquement pour les lignes modifiées entre deux contributions sur &lt;em&gt;Git&lt;/em&gt;, ce qui permet d'accélérer grandement l'exécution ;&lt;/li&gt;
&lt;li&gt;Génération d'un rapport en &lt;em&gt;JSON&lt;/em&gt; pour des traitements a posteriori ;&lt;/li&gt;
&lt;li&gt;Possibilité de déclarer un objectif pour le score de mutation. Le code de sortie d'une exécution est un code d'erreur si l'objectif n'est pas atteint (utile par exemple dans une chaîne &lt;em&gt;CI/CD&lt;/em&gt;) ;&lt;/li&gt;
&lt;li&gt;Les tests peuvent passer en &lt;code&gt;TIMEOUT&lt;/code&gt; s'ils prennent trop de temps à être exécutés ; Cela permet d'accélérer l'exécution du test de mutation mais dégrade la précision du test de mutation. Le temps-limite est calculé avec le temps d'exécution du plan de test sur le code source original, et un facteur multiplicatif.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Résumé
&lt;/h3&gt;

&lt;p&gt;Nous avons passé en revue plusieurs outils pour automatiser le test de mutation en Go. D'un côté, &lt;em&gt;manbearpig&lt;/em&gt; et &lt;em&gt;mutator&lt;/em&gt; sont d'anciens projets qui ne sont pas tenus à jour ; de l'autre, &lt;em&gt;ooze&lt;/em&gt;, &lt;em&gt;go-mutesting&lt;/em&gt;, et &lt;em&gt;gremlins&lt;/em&gt; sont des outils aux fonctionnalités similaires.&lt;/p&gt;

&lt;p&gt;Je propose quelques constats :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le score de mutation calculés par chaque outil est différent. C'est dû aux mutateurs utilisés : tous les outils n'évaluent pas les mêmes mutants. Le score de mutation n'est donc pas comme la couverture de code par les tests : sa valeur dépend beaucoup de l'outil de mesure. Cependant, je ne pense pas qu'un plan de test donnant un score médiocre avec un outil puisse donner un score excellent avec un autre. Il y a juste des outils un peu plus &lt;em&gt;optimistes&lt;/em&gt; que d'autres ;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;gremlins&lt;/em&gt; et &lt;em&gt;go-mutesting&lt;/em&gt; génèrent des rapports utilisables par ailleurs, par exemple pour afficher les lignes de code faiblement testées dans un &lt;em&gt;IDE&lt;/em&gt;. Cependant, ces rapports suivent des conventions différentes ;&lt;/li&gt;
&lt;li&gt;à part &lt;em&gt;ooze&lt;/em&gt;, il est nécessaire de contribuer au développement de l'outil, ou d'en créer une version alternative, pour ajouter un mutateur ;&lt;/li&gt;
&lt;li&gt;aucun de ces outils ne permet une utilisation incrémentale, c'est-à-dire que chaque exécution de ces outils entraîne l'analyse intégrale du code source. Certes, &lt;em&gt;gremlins&lt;/em&gt; a une option &lt;code&gt;diff&lt;/code&gt; pour restreindre l'analyse à une portion du code, mais cela ne permet pas de recalculer le score de mutation pour l'ensemble du projet ;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;gremlins&lt;/em&gt; est le seul outil de cette revue qui fait des compromis entre la précision du score de mutation et le temps d'exécution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;À présent munis d'un moyen d'automatiser le test de mutation, nous pouvons mener de nouvelles investigations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Des projets populaires à l'épreuve de la mutation
&lt;/h2&gt;

&lt;p&gt;Pour finir cet article, nous pouvons enfin revenir à la question initiale : avons-nous raison de faire confiance à nos logiciels préférés ?&lt;/p&gt;

&lt;p&gt;Nous allons y répondre sous le prismes des notions présentées plus haut, et en nous cantonnant aux projets écrits en Go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Objectif
&lt;/h3&gt;

&lt;p&gt;L'objectif est de vérifier si le plan de test des projets (logiciels, bibliothèques) populaires en Go sont de bonne qualité.&lt;/p&gt;

&lt;p&gt;Les projets que nous allons considérer ici sont les logiciels et les bibliothèques écrits en Go (et non, p. ex., les tutoriels) et hébergés sur GitHub.&lt;br&gt;
Pour simplifier la notion de popularité d'un projet, nous allons considérer leur nombre d'étoiles sur GitHub. Nous nous intéressons donc aux projets ayant le plus d'étoiles.&lt;br&gt;
Comme présenté plus haut, nous allons aussi réduire la qualité d'un plan de test à sa couverture de code et son score de mutation. Nous allons devoir définir ce qui est une valeur élevée pour ces deux mesures. Pour la suite, je choisis arbitrairement que :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;une couverture de code par les tests élevée est d'au moins 80% (ce qui laisse tout de même 20% du code source libre de tout contrôle) ;&lt;/li&gt;
&lt;li&gt;un score de mutation élevé est d'au moins 80% (ce qui signifie tout de même que le plan de test « &lt;em&gt;laisse passer&lt;/em&gt; » 20% des variantes générées à partir du code source).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;L'hypothèse que nous allons vérifier est la suivante : un projet populaire a mérité sa popularité en démontrant sa qualité à travers un plan de test de bonne qualité. Nous allons raffiner cette hypothèse en deux sous-hypothèses :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;un projet populaire a une haute couverture de code par les tests ;&lt;/li&gt;
&lt;li&gt;un projet populaire a un haut score de mutation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si un projet a une haute couverture de code par les tests et un score de mutation médiocre, alors un grand nombre de ses tests sont bidons. S'il a une faible couverture et un grand score de mutation, alors la petite portion du code testée peut être jugée fiable. Si ces deux métriques sont faibles, nous ne pouvons rien tirer du plan de test. Dans ces trois cas, la popularité du projet doit être expliquée par autre chose que sa qualité (p. ex. la publicité, la mode).&lt;/p&gt;
&lt;h3&gt;
  
  
  Échantillonnage des projets
&lt;/h3&gt;

&lt;p&gt;Pour commencer, nous devons lister les projets populaires à analyser. Prenons les 300 projets de la catégorie Go sur GitHub qui ont le plus d'étoiles. Pour lister leurs URL, utilisons l'outil en ligne de commande de GitHub :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;gh search repos --topic go --sort stars --limit 300 --json url | jq -r ".[].url"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cela aboutit... à une liste de 300 liens. Tous ne nous intéressent pas. En effet :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;certains des dépôts contiennent des exemples et des tutoriels, avec certes du code Go mais dont on n'attend pas qu'il soit testé pour que les gens veuillent bien l'utiliser ;&lt;/li&gt;
&lt;li&gt;d'autres sont des agrégations de sources diverses, des livres ou bien des logiciels écrits dans un autre langage que Go : bref, il n'y a pas de code Go à tester.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Une première passe nous permet d'en éliminer &lt;a href="https://arjca.fr/d/exp%C3%A9rience-mutation-go/d%C3%A9p%C3%B4ts-ignor%C3%A9s.txt" rel="noopener noreferrer"&gt;34&lt;/a&gt;. Il reste donc &lt;a href="https://arjca.fr/d/exp%C3%A9rience-mutation-go/d%C3%A9p%C3%B4ts.txt" rel="noopener noreferrer"&gt;266 dépôts à analyser&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mesures réalisées
&lt;/h3&gt;

&lt;p&gt;Pour investiguer les projets, j'ai utilisé quelques outils :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nombre d'étoiles sur GitHub : &lt;code&gt;gh&lt;/code&gt; (l'outil en ligne de commande de GitHub) ;&lt;/li&gt;
&lt;li&gt;couverture de code par les tests : l'outillage inclus dans la distribution de Go ;&lt;/li&gt;
&lt;li&gt;score de mutation : &lt;code&gt;gremlins&lt;/code&gt;, car même s'il aboutit à des scores de mutation un peu plus élevés que les autres, ses fonctionnalités m'ont permis de réaliser l'analyse de tous les projets dans un temps « &lt;em&gt;raisonnable&lt;/em&gt; ».&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;J'ai réalisé les calculs en plusieurs étapes.&lt;/p&gt;

&lt;p&gt;D'abord, j'ai réalisé une première passe des projets pour en extraire les informations les plus rapides à produire, notamment :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;leur nombre d'étoiles sur GitHub ;&lt;/li&gt;
&lt;li&gt;leur nombre de lignes de code ;&lt;/li&gt;
&lt;li&gt;la durée d'exécution du plan de test ;&lt;/li&gt;
&lt;li&gt;la couverture de code par les tests.
Cette étape est importante car, si exécuter le plan de test est supposé être rapide, nous allons devoir l'exécuter de très nombreuses fois pour évaluer le score de mutation : avec des moyens matériels limités pour réaliser l'étude, nous devons faire des choix. Par exemple, exécuter le plan de test du projet &lt;a href="https://github.com/aws/aws-sdk-go" rel="noopener noreferrer"&gt;&lt;em&gt;aws-sdk-go&lt;/em&gt;&lt;/a&gt; prend près d'une minute et demi sur ma machine ; or, le projet compte presque six millions de lignes : le plan de test risque d'être exécuté des millions de fois, ce qui peut être déraisonnablement long.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ensuite, pour chaque projet retenu, j'ai exécuté les tests de mutation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Résultats
&lt;/h3&gt;

&lt;p&gt;De nombreux jours de calcul plus tard, voici venue l'heure des résultats ! (&lt;a href="https://arjca.fr/d/exp%C3%A9rience-mutation-go/r%C3%A9sultats.csv" rel="noopener noreferrer"&gt;ici, les résultats compilés au format &lt;em&gt;CSV&lt;/em&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Premières remarques :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;le fichier contient 256 entrées, soit 10 de moins qu'attendu. En effet, j'ai compté une entrée par module Go dans les dépôts, et certains n'en avaient aucun. Néanmoins, certains autres en avaient plusieurs (voir la colonne &lt;code&gt;Folder&lt;/code&gt;) ;&lt;/li&gt;
&lt;li&gt;les tests ont échoué dans 129 modules, soit un peu plus de la moitié des modules analysés. Dans la plupart des cas, c'est lié à des dépendances attendues sur la machine qui exécute les tests : par exemple, les tests de &lt;a href="https://github.com/kubernetes/minikube" rel="noopener noreferrer"&gt;&lt;em&gt;minikube&lt;/em&gt;&lt;/a&gt; sont dépendants de &lt;em&gt;libvirt&lt;/em&gt;. Étant donné le nombre de projets à analyser, je n'ai pas étudié chacun dont les tests sont en échec. J'ai décidé de simplement les mettre de côté ;&lt;/li&gt;
&lt;li&gt;j'ai mis huit modules de côté car, étant donné leur taille et la durée d'exécution de leurs tests, cela aurait été trop long de calculer leur score de mutation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finalement, j'ai calculé la couverture de code par les tests de 125 modules, et le score de mutation de 117 d'entre eux.&lt;/p&gt;

&lt;p&gt;D'abord, jetons un œil sur un premier graphique avec des données « &lt;em&gt;démographiques&lt;/em&gt; » sur nos projets. Les projets sélectionnés sont assez variés, autant dans leur nombre de lignes de code que dans leur nombre d'étoiles sur GitHub. La variance est immense. Nous n'observons pas de corrélation entre le nombre de lignes de code et la popularité des projets, ni entre le nombre de tests et leur popularité. Cela n'est pas très étonnant. En revanche, il n'apparaît pas non plus de corrélation entre le nombre de tests et le nombre de lignes de code, ce qui est curieux : à voir s'il s'agit de surqualité dans certains petits projets avec beaucoup de tests, ou de sous-qualité dans les autres.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6bwpfnkr8xarapmhopf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs6bwpfnkr8xarapmhopf.png" alt="graphique avec le nombre de tests par nombre de lignes de code des modules analysés" width="622" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ensuite, intéressons-nous à la couverture de code par les tests. Le graphique ci-dessous montre plusieurs choses :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nous identifions deux pics : un premier entre 50% et 60% de couverture de code par les tests, et un autre un peu moins grand autour de 20% ;&lt;/li&gt;
&lt;li&gt;les plus grands projets en nombre de lignes de code (&lt;em&gt;LOC&lt;/em&gt; &amp;gt; 100000) ont pour la quasi-totalité une couverture de code par les tests inférieure à 60% ;&lt;/li&gt;
&lt;li&gt;seuls 22 modules ont 80% de couverture ou plus, soit environ 18% des modules analysés.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cela permet de trancher pour la première hypothèse : non, les projets écrits en Go les plus populaires n'ont pas une bonne couverture de code par les tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6udvgiek1m3bxsxd6267.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6udvgiek1m3bxsxd6267.png" alt="graphique avec la couverture de code par les tests en fonction du nombre de lignes des modules analysés" width="588" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enfin, voyons les résultats des tests de mutation. À nouveau, il est difficile d'établir la moindre corrélation à partir de cette figure. Cependant, il est évident que si de nombreux modules n'ont pas une grande couverture de code par le test, le score de mutation est, lui, plutôt élevé en général.&lt;br&gt;
Remarquons toutefois que les projets avec une bonne couverture ont aussi systématiquement un score de mutation élevé (&amp;gt; 60%), ce qui n'est pas le cas de certains projets avec une moindre couverture.&lt;/p&gt;

&lt;p&gt;Pour revenir à notre hypothèse, nous comptons 49 modules avec un score de mutation supérieur à 80%, soit presque 42% des projets analysés. Valider notre hypothèse dans ces conditions serait exagéré. Notons tout de même la grande concentration des projets autour de ce niveau, 80% : 66% des modules ont un score de mutation supérieur à 70%, et plus de 13% des modules ont un score supérieur à 90%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F142e456erj3tvjhu2d0n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F142e456erj3tvjhu2d0n.png" alt="graphique avec le score de mutation en fonction de la couverture de code par les tests des modules analysés" width="588" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Résumé et rétrospective sur l'utilisation du test de mutation
&lt;/h3&gt;

&lt;p&gt;En évaluant la qualité des plans de tests des projets écrits en Go les plus populaires, nous avons conclu qu'avoir un bon plan de test n'est pas un prérequis à la popularité. La plupart de ces projets ont une couverture en deçà des exigences communes de l'industrie.&lt;/p&gt;

&lt;p&gt;En plus de cet aperçu de l'écosystème Go, cette investigation a mis en lumière des difficultés vis-à-vis du test de mutation :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ce type de test est gourmand autant en calcul, en mémoire vive et en stockage sur disque dur. Par conséquent, d'importants moyens sont requis pour le mettre en œuvre sur de grands projets ;&lt;/li&gt;
&lt;li&gt;le temps d'exécution des tests de mutation est parfois très grand, et complètement tributaire de la taille du projet et de la rapidité d'exécution du plan de test. À titre d'exemple, le test de mutation pour le projet &lt;a href="https://github.com/minio/minio" rel="noopener noreferrer"&gt;&lt;em&gt;minio&lt;/em&gt;&lt;/a&gt; a duré près de 2 jours sur ma machine ;&lt;/li&gt;
&lt;li&gt;un score de mutation indique que &lt;em&gt;au plus&lt;/em&gt; un certain pourcentage des tests ne valide pas uniquement le code source du logiciel. La précision « &lt;em&gt;au plus&lt;/em&gt; » est due aux potentiels mutants équivalents générés dans le processus ; difficile de savoir combien il y en a à chaque fois : cela demanderait une étude de chaque mutant survivant ;&lt;/li&gt;
&lt;li&gt;l'outillage pour l'écosystème Go n'est pas encore idéal. D'après moi, il manque certaines fonctionnalités de confort et d'autres purement pratiques pour pouvoir être démocratisé dans l'industrie.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Les fonctionnalités que j'aimerais voir dans ces outils dans le futur sont les suivantes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reprendre un test de mutation interrompu sans exécuter à nouveau les mutants déjà évalués (p. ex. pour pouvoir lancer les tests de mutation sur une machine préemptible sur &lt;em&gt;Google Compute Engine&lt;/em&gt;) ;&lt;/li&gt;
&lt;li&gt;Restreindre les tests exécutés à ceux du paquetage contenant le code muté (afin de gagner du temps à l'exécution du test de mutation et à l'analyse des mutants survivants) ;&lt;/li&gt;
&lt;li&gt;Suivre la progression d'un test de mutation (p. ex. avec une barre de progression qui indique le nombre de mutants déjà évalués, sur le nombre de mutants total) ;&lt;/li&gt;
&lt;li&gt;Utiliser le test de mutation au fur et à mesure des évolutions, sans avoir à effectuer à nouveau l'intégralité du processus à chaque fois.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Dans ce (finalement) long article, nous avons rappelé les bases du test logiciel, de pourquoi nous le faisons et d'une question que les programmeurs devraient se poser : quels tests sont nécessaires et suffisants ? Pas de réponse claire à cette question, seulement des indices. L'un d'entre eux est la couverture de code par les tests, une mesure commune dans l'industrie pour évaluer la qualité d'un plan de test. Cependant, cette mesure n'est pas fiable seule, c'est pourquoi nous pourrions l'accompagner du score de mutation.&lt;br&gt;
Plusieurs outils existent dans l'écosystème Go pour automatiser le calcul du score de mutation. Nous nous en sommes servi sur des projets très populaires à titre d'exemple.&lt;br&gt;
Cette expérience nous a montré que, si ces projets sont très utilisés, ce n'est sans doute pas parce qu'ils ont fait la preuve de leur qualité dans leur plan de test. Une hypothèse pour expliquer ces manques : pas assez de moyens ; cependant, les importants scores de mutation que nous avons calculés devraient conforter l'idée que le travail réalisé est de bonne facture. En tant qu'informaticien, il est important que nous questionnions les outils de notre quotidien, et ne pas nous laisser influencer par la publicité. Plutôt que d'être de simples consommateurs, à nous de contribuer à ces outils pour les rendre plus sûrs et accroître leur qualité, sinon leurs fonctionnalités.&lt;/p&gt;

&lt;p&gt;En guise d'ouverture, je souhaitais souligner que la couverture de code par les tests et le score de mutation sont deux mesures pour évaluer un certain aspect de la qualité d'un plan de test. Ils ne suffisent pas, puisqu'ils sont complètement indépendants de la spécification. À l'avenir, nous pourrions réfléchir à une nouvelle mesure qui établi à quelle point la spécification d'un logiciel est couverte par un plan de test : la &lt;em&gt;couverture de spécification par les tests&lt;/em&gt;. En attendant, le développement dirigé par les tests (&lt;em&gt;TDD&lt;/em&gt;) est une pratique à envisager car, d'une part, elle devrait aboutir à une couverture de code par les tests proche de 100% sans falsification et, d'autre part, c'est la spécification qui dirige la création des cas de test. À ce sujet, je vous recommande de visionner &lt;a href="https://www.youtube.com/watch?v=EZ05e7EMOLM" rel="noopener noreferrer"&gt;la présentation que Ian Cooper a donné en 2017 à la conférence DevTernity&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Merci d’avoir lu mon article ! Je suis Antoine, ingénieur &lt;em&gt;cloud&lt;/em&gt; chez &lt;a href="https://stack-labs.com/" rel="noopener noreferrer"&gt;&lt;em&gt;Stack Labs&lt;/em&gt;&lt;/a&gt;. Si vous voulez en savoir plus sur Stack Labs ou rejoindre une équipe de passionnés de tech, n’hésitez pas à nous contacter &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;ici&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;Crédits à Games Workshop® pour l'image de couverture.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>go</category>
    </item>
    <item>
      <title>Cloud : un peu d’ARM avec votre cluster Kubernetes ?</title>
      <dc:creator>Λ\: Laurent Noireterre</dc:creator>
      <pubDate>Mon, 06 May 2024 12:10:04 +0000</pubDate>
      <link>https://dev.to/stack-labs/cloud-un-peu-darm-avec-votre-cluster-kubernetes--36aa</link>
      <guid>https://dev.to/stack-labs/cloud-un-peu-darm-avec-votre-cluster-kubernetes--36aa</guid>
      <description>&lt;p&gt;La majorité des clouds providers proposent des solutions basées sur des architectures processeurs &lt;strong&gt;ARM&lt;/strong&gt;, tel que &lt;strong&gt;Graviton&lt;/strong&gt; chez AWS ou &lt;strong&gt;Tau T2A&lt;/strong&gt; chez GCP. Les avantages de tels processeurs sont multiples : efficacité énergétique, couts réduits, performances… Ils sont de plus tout à fait adaptés aux environnements conteneurisés.&lt;/p&gt;

&lt;p&gt;Exécuter vos workloads &lt;strong&gt;Kubernetes&lt;/strong&gt; sur des processeurs ARM parait donc être un bonne idée. Cela rentre aussi dans une approche &lt;strong&gt;FinOps&lt;/strong&gt;, car l’utilisation de processeurs arm en lieu et place de processeurs x86 va permettre des réductions de coûts non négligeables (de l’ordre de 20% avec des processeurs Graviton) à performances égales voire supérieures. &lt;/p&gt;

&lt;p&gt;Si la mise en place d’une architecture arm peut paraitre assez simple sur de nouveaux clusters, qu’en est-il de la migration d’une architecture existante amd64 vers une architecture arm64 ? &lt;/p&gt;

&lt;h2&gt;
  
  
  Considérations
&lt;/h2&gt;

&lt;p&gt;Avant de se lancer dans la migration vers une architecture arm, quelques considérations sont à prendre en compte vis-à-vis des applications qui tournent sur votre cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Langages et librairies
&lt;/h3&gt;

&lt;p&gt;La majorité des langages supportent maintenant des architectures ARM. Les langages interprétés (NodeJS, Python…) ou byte-code compilés (Java, .Net) devraient fonctionner sans modifications majeures. Attention cependant si vous utilisez des librairies ou fragments de codes natifs (JNI), une recompilation sera necessaire.&lt;/p&gt;

&lt;p&gt;Les langages compilés (C/C++, Go…) supportent pour la plus grande majorité les architectures ARM mais ils devront être recompilés.&lt;/p&gt;

&lt;h3&gt;
  
  
  Images Docker
&lt;/h3&gt;

&lt;p&gt;De manière générale nos applications packagées pour s’executer dans des conteneurs utilisent une image de base (le &lt;strong&gt;FROM&lt;/strong&gt; du Dockerfile). Attention à bien vérifier que cette image aussi supporte ARM. C’est le cas de la majorité des standards et cela peut se vérifier rapidement en se connectant sur le registry depuis lequel elles sont tirées. Par exemple pour l’image officielle OpenJDK sur Dockerhub, on remarque que les 2 types d’architectures sont bien supportées :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8fi4wv0uo9wz70y4jg5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy8fi4wv0uo9wz70y4jg5.png" alt="Image description" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Services tiers
&lt;/h3&gt;

&lt;p&gt;Des services tiers tels que Prometheus ou ArgoCD peuvent aussi tourner sur nos cluster Kubernetes afin d’assurer diverses taches (observabilité, déploiement, sécurité…). Il faudra donc s’assurer là aussi que ces services sont déployables sur une architecture ARM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Système d’exploitation
&lt;/h3&gt;

&lt;p&gt;Si vous utilisez des services cloud entièrement managés tel que &lt;strong&gt;EKS Fargate&lt;/strong&gt; ou &lt;strong&gt;GKE Autopilot&lt;/strong&gt; il n’y aura aucun impact. Par contre si vous avez des noeuds que vous managez vous-même, une migration du système d’exploitation sera nécessaire. &lt;/p&gt;

&lt;h2&gt;
  
  
  Construction : Docker multi-architecture
&lt;/h2&gt;

&lt;p&gt;Maintenant que vous vous êtes assuré que vos applications sont bien éligibles à une plateforme arm, il s’agit de les reconstruire afin qu’elles puissent tourner sur ce type d’architecture. &lt;/p&gt;

&lt;h3&gt;
  
  
  Principe
&lt;/h3&gt;

&lt;p&gt;La meilleure solution pour pouvoir instancier vos conteneurs sur une architecture ARM n’est pas d’effectuer un build de vos images pour ce type d’architecture spécifiquement, mais plutôt d’utiliser une méthode de build &lt;strong&gt;multi-architecture&lt;/strong&gt;. Votre image sera alors construite en même temps et à partir de la même source (DockerFile) pour une liste d’architecture que vous aurez prédéfinie.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17q049m1rluqkb1ra10k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17q049m1rluqkb1ra10k.png" alt="Image description" width="341" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cela permettra de déployer ces nouvelles images sur vos clusters nouvellement configurés avec des noeuds arm, mais aussi de pouvoir lancer indifféremment vos containers sur des architectures plus classiques type amd64 pour du développement ou des tests par exemple.&lt;/p&gt;

&lt;p&gt;Ce sera le runtime de conteneur qui, au moment du pull, récupérera les layers de l’image correspondant au type d’architecture sur lequel il tourne : &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftadeyrq9mbtg592hqf3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftadeyrq9mbtg592hqf3j.png" alt="Image description" width="644" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mise en place
&lt;/h3&gt;

&lt;h3&gt;
  
  
  Solution 1 : Docker buildx
&lt;/h3&gt;

&lt;p&gt;Une approche courante pour construire des images Docker multi-architecture consiste à utiliser le plugin docker &lt;strong&gt;buildx&lt;/strong&gt; (&lt;a href="https://docs.docker.com/build/architecture/#buildx" rel="noopener noreferrer"&gt;https://docs.docker.com/build/architecture/#buildx&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Ce plugin se base sur &lt;strong&gt;QEMU&lt;/strong&gt; (Quick Emulator) pour construire des images multi-architectures. QEMU est un émulateur de processeur qui permet d'exécuter du code destiné à une architecture spécifique depuis un autre type d’architecture. Cela va permettre de construire des images Docker pour des architectures différentes de celle de l'hôte.&lt;/p&gt;

&lt;p&gt;Concrètement, tout ce que vous aurez à faire est d’installer le plugin docker buildx (vérifier la compatibilité avec votre version de docker), et de lancer un build en listant les plateformes cibles :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx create &lt;span class="nt"&gt;--use&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mybuild node-amd64
mybuild
docker buildx create &lt;span class="nt"&gt;--append&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mybuild node-arm64
docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le build et le push de l’image peuvent se faire avec une seule et même instruction :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="nt"&gt;--tag&lt;/span&gt; my-user/my-image &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/arm64/v8,linux/amd64 &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour plus de détails vous pouvez vous référer à la procédure Docker: &lt;a href="https://docs.docker.com/build/building/multi-platform/" rel="noopener noreferrer"&gt;https://docs.docker.com/build/building/multi-platform/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2 : Manisfest
&lt;/h3&gt;

&lt;p&gt;Une seconde solution, plus complexe, est la méthode “Do It Yourself”. Elle consiste à une création manuelle du manifest d'image après avoir effectué 2 builds, un pour chaque type d’architecture.&lt;/p&gt;

&lt;p&gt;Elle fait aussi intervenir 3 registres d’images, car chacune des images construites doit être poussée dans son propre registre avant d’être poussée une 3ème fois dans un registre multi-architecture.&lt;/p&gt;

&lt;p&gt;Donc pour résumer :&lt;/p&gt;

&lt;p&gt;1 - On construit et on pousse une image pour chaque architecture&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AMD64&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-user/my-image-amd64 &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64/ &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker push my-user/my-image-amd64

&lt;span class="c"&gt;# ARM64V8&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; my-user/my-image-arm &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm64v8/ &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;docker push my-user/my-image-arm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2 - On créé un manifeste à partir de chacune des images&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker manifest create &lt;span class="se"&gt;\&lt;/span&gt;
my-user/my-image &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--amend&lt;/span&gt; my-user/my-image-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--amend&lt;/span&gt; my-user/my-image-arm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3 - On pousse le nouveau manifest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker manifest push my-user/my-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Remarque&lt;/em&gt; : il est aussi possible de jouer sur les tags des différentes images pour n’utiliser qu’un seul registry&lt;/p&gt;

&lt;p&gt;Cette solution peut être utile si vous construisez vos images avec un autre outils que Docker, tels que Kaniko ou Buildah. &lt;/p&gt;

&lt;p&gt;Un post sur le blog de Docker détaille ces 2 méthodes: &lt;a href="https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/" rel="noopener noreferrer"&gt;https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting : Cluster Kubernetes Hybride
&lt;/h2&gt;

&lt;p&gt;Vous avez maintenant des images Docker multi-architectures capable de tourner indifféremment sur de l’amd64 ou de l’arm. Mais il est possible que pour une raison ou une autre, certaines de vos applications n’aient pas pu être construites pour architecture arm et que vous deviez donc conserver des noeuds type amd64 pour celles-ci.&lt;/p&gt;

&lt;p&gt;Dans ce cas pas de panique, vous pouvez jouer sur les teintes et node selector (ou node affinity) de Kubernetes. &lt;/p&gt;

&lt;p&gt;Je m’explique. Les clusters Kubernetes managés sont capables de gérer plusieurs groupes de noeuds, chacun de ces groupes pouvant s’appuyer sur des propriétés différentes (type d’instance, nombre d’instances…). Il est alors tout à fait possible de créer 2 groupes de noeuds distincts, l’un comportant des machines de type amd64 et l’autre de type arm. Pour garder la maitrise sur les workloads qui vont être déployés par la suite sur l’un ou l’autre groupe de noeuds, on appliquera une teinte sur l’un des groupes :&lt;/p&gt;

&lt;p&gt;(Si vous n’êtes pas familier avec la notion de teinte et de node selector je vous invite à consulter à la documentation Kubernetes &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/&lt;/a&gt; et &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nvrvr5r7nic0d81a4zk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nvrvr5r7nic0d81a4zk.png" alt="Image description" width="800" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Par défaut tous nos pods seront ainsi déployés sur le node group arm. On utilisera alors un node selector ainsi qu’une tolération au niveau des pods que l’on souhaite assigner à un type d’architecture amd64.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tolerations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NoSchedule&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-arch-type&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
&lt;span class="na"&gt;nodeSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kubernetes.io/arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cela permet de continuer de faire tourner sans risque vos applications nos compatibles arm sur des noeuds amd64.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Remarque&lt;/em&gt;: on peut tout à fait envisager le mécanisme inverse, à savoir un déploiement par défaut sur des noeuds type amd64 (non teintés) et prévoir une sélection d’applications à déployer sur des noeuds type arm (grâce aux teintes et node selectors). Cela permet dans un cluster existant amd64, d’envisager une stratégie de migration de vos applications par lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Félicitations !&lt;/strong&gt; Vous avez maintenant un cluster Kubernetes capable d’héberger plusieurs types d’architectures, et des applications pouvant se déployer indifféremment sur l’une ou l’autre tout en gérant vous même cette répartition.&lt;/p&gt;

&lt;p&gt;Ce type d'architecture de plus en plus répandu mérite vraiment de s'y intéresser, surtout dans le cas de la mise en place d'une nouvelle plateforme. &lt;/p&gt;

&lt;p&gt;Pour ce qui est de la migration d'une plateforme existante, le ROI est de manière générale très intéressant mais une stratégie de migration doit impérativement être mise en place. &lt;/p&gt;

</description>
      <category>cloud</category>
      <category>kubernetes</category>
      <category>arm</category>
      <category>docker</category>
    </item>
    <item>
      <title>Applying graph theory for inferring your BigQuery SQL transformations: an experimental DataOps tool</title>
      <dc:creator>Λ\: Clément Bosc</dc:creator>
      <pubDate>Tue, 16 Apr 2024 20:26:40 +0000</pubDate>
      <link>https://dev.to/stack-labs/applying-graph-theory-for-inferring-your-bigquery-sql-transformations-an-experimental-dataops-tool-463n</link>
      <guid>https://dev.to/stack-labs/applying-graph-theory-for-inferring-your-bigquery-sql-transformations-an-experimental-dataops-tool-463n</guid>
      <description>&lt;p&gt;If you work with Google Cloud for your Data Platform there are chances that you use BigQuery and run your Data pipelines transformations in a ELT manner: using BQ query engine to run transformations as a series of SELECT statements, one after another. Indeed over the last few years, ELT and tools like DBT or Dataform have been the de-facto standard for running and organizing your Data transformations at scale.&lt;/p&gt;

&lt;p&gt;Theses tools, that we may group under the “SQL orchestration tools” banner are great for many reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL is the main and only language to express a transformation, indeed SQL is great for structured data (and even semi-structured data)&lt;/li&gt;
&lt;li&gt;They do a great job at centralizing the transformations: nice for audits, lineage tracking and trust&lt;/li&gt;
&lt;li&gt;They simplify the DataOps experience and help onboard Data Analysts in Data Engineer tasks&lt;/li&gt;
&lt;li&gt;They can almost automatically infer the transformation dependencies by creating a DAG. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BUT, for my Platform Engineering background, they have a major flow: they miss a state. Indeed if you take declarative IaC tools like Terraform, the current state of the Data Platform infrastructure is stored in a file (the state), including the tables/views, the permissions etc...&lt;/p&gt;

&lt;h2&gt;
  
  
  But how is this a problem ?
&lt;/h2&gt;

&lt;p&gt;The problem is that tools like DBT or Dataform are only running DML statements. For example to create a table the generated statement will be &lt;code&gt;CREATE OR REPLACE TABLE AS SELECT your_transformation&lt;/code&gt;. This means that the tool never knows if the object exists before or not, so you cannot attach IAM permission on it with Terraform (because the object is re-created every day in your daily batch) neither can you use the table as agreement interface with consumers because the table does not exists prior to the transformation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: an experimental tool that use the best of both worlds
&lt;/h2&gt;

&lt;p&gt;I wanted to keep the benefits from SQL orchestration tools (like Dataform on GCP), but in conjunction with Terraform for the Ops benefits, by keeping in mind the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table dependencies between 2 transformations (running the transformation B after the A if B reference table A in the query) should be &lt;strong&gt;automatically inferred&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Table schema (type, column_name) must be &lt;strong&gt;automatically inferred&lt;/strong&gt;: user should not lose time on writing the table schema if it can be deduced from the output.&lt;/li&gt;
&lt;li&gt;Table should be automatically created prior to the transformation (not by the transformation) with an IaC tool : Terraform&lt;/li&gt;
&lt;li&gt;Be able to have a custom monitoring interface that gathers all the transformations information: status, cost, performance, custom error messages etc..&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture proposal
&lt;/h2&gt;

&lt;p&gt;Here is the architecture proposal for my experimental transformation DataOps-oriented tool&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transformation are BigQuery queries&lt;/li&gt;
&lt;li&gt;Orchestration is carried by an auto-generated Cloud Workflow with all the correct dependencies and parallel steps when possible (if two transformations can run at the same time)&lt;/li&gt;
&lt;li&gt;Monitoring is a BigQuery timestamp-partitioned table with a Pub/Sub topic (and an Avro schema for the interface) and a push-to-BQ streaming subscription&lt;/li&gt;
&lt;li&gt;Transformations are defined in a git repository in yaml files. Jinja template are supported for flexibility and factorisation)&lt;/li&gt;
&lt;li&gt;A Cloud Run endpoint that host all the schema/dependencies inference logic and Workflow body generation according to the transformation dependencies (more on the Cloud Run below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19r1krgme0i3jj44la4s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F19r1krgme0i3jj44la4s.png" alt="Automatically create BigQuery tables with Terraform" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to infer dependencies ?
&lt;/h2&gt;

&lt;p&gt;Here is where the magic happens : the automatic dependency inference. Let’s remind it, DAG in data pipelines are nothing more than Graphs (Direct Acyclic Graphs), so let’s use a Graph library to build them from raw SQL declarations. You can find all the detailed process and Python implementation examples in this post: &lt;a href="https://dev.to/stack-labs/build-the-dependency-graph-of-your-bigquery-pipelines-at-no-cost-a-python-implementation-1ik1"&gt;Build the dependency graph of your BigQuery pipelines at no cost: a Python implementation&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The raw SQL declarations are sent by Terraform to a remote Cloud Run instance that computes the inference logic (DAG creation, Workflows source code generation, table schema generation), so Terraform that immediately creates the tables and workflows, prior to any transformations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exemple: the experiment in action
&lt;/h2&gt;

&lt;p&gt;Let’s take a simple example: we are in a standard Data Platform Architecture with a 3 layer principal: Bronze (raw data), Silver (curated data) and Gold (aggregated/meaningful data). We need to run a data transformation pipeline, in SQL, that cleans the raw data (for deduplication and type-conversion for ex) and builds an analytics-ready aggregated table from the cleaned data.&lt;/p&gt;

&lt;p&gt;The demo dataset is a very simple retail-oriented data model (orders, products and users), orders being the fact table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzi2cpty6hmj68amy0ibd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzi2cpty6hmj68amy0ibd.png" alt="Infer BigQuery table dependencies" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our tool, based on Terraform, needs to create the SILVER and GOLD tables, with the correct schemas, ahead of the transformations running, and the Cloud Workflow source definition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data transformation files:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The transformations are described in a yaml file, specifying the destination table and the SQL transformation query as a single select.&lt;/p&gt;

&lt;p&gt;Building the silver layer, here it’s only a deduplication step for the sake of the demo&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;workflow_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo&lt;/span&gt;
&lt;span class="na"&gt;destination_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${raw_project}&lt;/span&gt;
  &lt;span class="na"&gt;dataset_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${app_name}_ds_3_demo_${multiregion_id}_${project_env}&lt;/span&gt;
  &lt;span class="na"&gt;table_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EU&lt;/span&gt;

&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;SELECT&lt;/span&gt;
    &lt;span class="s"&gt;*&lt;/span&gt;
  &lt;span class="s"&gt;FROM `${raw_project}.sldp_demo_retail_analytics_raw_data_eu_${project_env}.orders_v1`&lt;/span&gt;
  &lt;span class="s"&gt;QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY insertion_time DESC) = 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building the gold layer, here an aggregated table of the total amount of sold products per month and consumer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;workflow_group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3-demo&lt;/span&gt;
&lt;span class="na"&gt;destination_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${raw_project}&lt;/span&gt;
  &lt;span class="na"&gt;dataset_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${app_name}_ds_3_demo_${multiregion_id}_${project_env}&lt;/span&gt;
  &lt;span class="na"&gt;table_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_cost_by_user&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EU&lt;/span&gt;

&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cost&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;by&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;month.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Granularity:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[user_id,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;month]"&lt;/span&gt;

&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="s"&gt;SELECT&lt;/span&gt;
    &lt;span class="s"&gt;u.email,&lt;/span&gt;
    &lt;span class="s"&gt;DATE_TRUNC(DATE(o.created_at), MONTH) as month,&lt;/span&gt;
    &lt;span class="s"&gt;SUM(o.quantity * p.price) as total_amount,&lt;/span&gt;
    &lt;span class="s"&gt;COUNT(DISTINCT o.id) as total_orders,&lt;/span&gt;
    &lt;span class="s"&gt;CURRENT_TIMESTAMP() as insertion_time&lt;/span&gt;
  &lt;span class="s"&gt;FROM `${raw_project}.${app_name}_ds_3_demo_${multiregion_id}_${project_env}.orders` o&lt;/span&gt;
  &lt;span class="s"&gt;JOIN `${raw_project}.${app_name}_ds_3_demo_${multiregion_id}_${project_env}.users` u&lt;/span&gt;
    &lt;span class="s"&gt;ON u.id = o.user_id&lt;/span&gt;
 &lt;span class="s"&gt;JOIN `${raw_project}.${app_name}_ds_3_demo_${multiregion_id}_${project_env}.products` p&lt;/span&gt;
    &lt;span class="s"&gt;ON p.id = o.product_id&lt;/span&gt;
  &lt;span class="s"&gt;GROUP BY email, month&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after running the terraform plan command we can see the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Terraform will perform the following actions:

&lt;span class="c"&gt;# google_bigquery_table.destination_tables["orders"] will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"google_bigquery_table"&lt;/span&gt; &lt;span class="s2"&gt;"destination_tables"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      + creation_time       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + dataset_id          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sldp_ds_3_demo_eu_dev"&lt;/span&gt;
      + schema              &lt;span class="o"&gt;=&lt;/span&gt; jsonencode&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;[&lt;/span&gt;
              + &lt;span class="o"&gt;{&lt;/span&gt;
                  + mode        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NULLABLE"&lt;/span&gt;
                  + name        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"id"&lt;/span&gt;
                  + &lt;span class="nb"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"INTEGER"&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;,
        ...
          &lt;span class="o"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# google_bigquery_table.destination_tables["products"] will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"google_bigquery_table"&lt;/span&gt; &lt;span class="s2"&gt;"destination_tables"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    ...
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# google_bigquery_table.destination_tables["users"] will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"google_bigquery_table"&lt;/span&gt; &lt;span class="s2"&gt;"destination_tables"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    ...
&lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="c"&gt;# google_bigquery_table.destination_tables["total_cost_by_user"] will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"google_bigquery_table"&lt;/span&gt; &lt;span class="s2"&gt;"destination_tables"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      + dataset_id          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sldp_ds_3_demo_eu_dev"&lt;/span&gt;
      + description         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Total cost by user and month. Granularity: [user_id, month]"&lt;/span&gt;
      + &lt;span class="nb"&gt;id&lt;/span&gt;                  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + schema              &lt;span class="o"&gt;=&lt;/span&gt; jsonencode&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;[&lt;/span&gt;
              + &lt;span class="o"&gt;{&lt;/span&gt;
                  + description &lt;span class="o"&gt;=&lt;/span&gt; null
                  + mode        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NULLABLE"&lt;/span&gt;
                  + name        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt;
                  + policyTags  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                      + names &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                  + &lt;span class="nb"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STRING"&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;,
              + &lt;span class="o"&gt;{&lt;/span&gt;
                  + description &lt;span class="o"&gt;=&lt;/span&gt; null
                  + mode        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NULLABLE"&lt;/span&gt;
                  + name        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"month"&lt;/span&gt;
                  + policyTags  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                      + names &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                  + &lt;span class="nb"&gt;type&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DATE"&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;,
        ...
     &lt;span class="o"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# google_workflows_workflow.data_transfo["3-demo"] will be created&lt;/span&gt;
  + resource &lt;span class="s2"&gt;"google_workflows_workflow"&lt;/span&gt; &lt;span class="s2"&gt;"data_transfo"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      + create_time      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + description      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + effective_labels &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + &lt;span class="nb"&gt;id&lt;/span&gt;               &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + name             &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"wkf_datatransfo_3_demo_euw1_dev"&lt;/span&gt;
      + name_prefix      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + project          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sldp-front-dev"&lt;/span&gt;
      + region           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"europe-west1"&lt;/span&gt;
      + revision_id      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;known after apply&lt;span class="o"&gt;)&lt;/span&gt;
      + service_account  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt;
      + source_contents  &lt;span class="o"&gt;=&lt;/span&gt; jsonencode&lt;span class="o"&gt;(&lt;/span&gt;
        &amp;lt;Coming from the Cloud Run backend mentioned above, called directly by terraform with the data http provider&amp;gt;
     &lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;


Plan: 5 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The auto-generated Cloud Workflow DAG:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the auto-generated Cloud Workflow, we can find 4 steps, one for each table. In our example above: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 can be done in parallel (the Silver tables) for deduplication and typing. Here we use the &lt;a href="https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.dag.topological_generations.html" rel="noopener noreferrer"&gt;topological generation&lt;/a&gt; method in our graph.&lt;/li&gt;
&lt;li&gt;1 step for the Gold transformation, that needs to wait for the termination of the previous steps, because the Silver tables are referenced by the Gold table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02i1mg2kk7lox9cs8764.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F02i1mg2kk7lox9cs8764.png" alt="Auto generated GCP Cloud Workflow DAG" width="725" height="699"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this Workflows, each step will do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile the query : in all our transformations we can use Jinja templating language. Workflows input parameters can be used in the transformation template. For example, we can use the “incremental” parameter to have a different transformation logic is we want to deal with incremental updates&lt;/li&gt;
&lt;li&gt;Run the BigQuery job (compiled query)&lt;/li&gt;
&lt;li&gt;Log the status of the job: the workflow publishes an event in a Pub/Sub topic that will dump in realtime in a BigQuery monitoring table, in order to track the status of every step and every workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  More features…
&lt;/h2&gt;

&lt;p&gt;The experiment is very feature rich now, here are some of the features we added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every transformation can have some SQL scripting pre-operations. The pre-operations are taken into account to process the dependency graph (if you create temporary tables for example) and are run into the same BQ session as the main transformation. BTW, checkout this great article by my friend Matthieu explaining the implementation in Python &lt;a href="https://dev.to/stack-labs/bigquery-transactions-over-multiple-queries-with-sessions-2ll5"&gt;BigQuery transactions over multiple queries, with sessions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;You can use Python Jinja tempting in every transformation by using some common variables that are available at run time : in the workflow, every transformation step is first “compiled” before being sent to BigQuery.&lt;/li&gt;
&lt;li&gt;You can define custom query templates that can be used across all the project: for example a Merge template is available for everyone to use to Implement merge strategy in the final table instead of replace/append.&lt;/li&gt;
&lt;li&gt;All templates can implement an incremental alternative (using Jinja conditions). For example, the Default template appends data to the final table if workflow is run in incremental mode or overwrites the data in non-incremental mode.&lt;/li&gt;
&lt;li&gt;All the input parameters of your workflows can be used in Jinja templates.&lt;/li&gt;
&lt;li&gt;After every workflow step, a real-time structured log information is being published to the monitoring Pub/Sub topic to be immediately streaming into the monitoring BQ table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;It works like a charm ! &lt;/p&gt;

&lt;p&gt;This architecture is being used for a few months internally at &lt;a href="https://stack-labs.com/" rel="noopener noreferrer"&gt;Stack Labs&lt;/a&gt; to process our internal data pipelines : there are extremely few pipeline errors at runtime (even less than with Dataform that sometimes lost the connection to the git provider), it’s very cost effective (the DAG generation is completely free thanks to a few hacks), the custom templating system is very flexible for advanced data engineering use cases and we now have proper custom monitoring logs at every transformation step to build real time monitoring dashboards !&lt;/p&gt;

&lt;p&gt;So yes, it’s a very geeky approach, and the developer experience is local-first and git-oriented, but if like me you have a Software Engineer background you will feel very comfortable doing Data Engineer/Analyst tasks using this approach. This will probably stay at the experimental phase, but it was fun designing a Serverless, DevOps-oriented Data Transformation and applying Graph theory in the solution. Feel free to ping me for the source code.&lt;/p&gt;

</description>
      <category>dataops</category>
      <category>bigquery</category>
      <category>googlecloud</category>
      <category>data</category>
    </item>
    <item>
      <title>Continuous Learning in Kubernetes: My Voyage of Discovery</title>
      <dc:creator>Hicham Yahiaoui</dc:creator>
      <pubDate>Mon, 15 Apr 2024 15:51:14 +0000</pubDate>
      <link>https://dev.to/stack-labs/continuous-learning-in-kubernetes-my-voyage-of-discovery-3mbb</link>
      <guid>https://dev.to/stack-labs/continuous-learning-in-kubernetes-my-voyage-of-discovery-3mbb</guid>
      <description>&lt;p&gt;In the vast expanse of computer science and technology, it's not uncommon to awaken one day and realize that the landscape has shifted dramatically, leaving you feeling somewhat behind the curve.&lt;br&gt;
Such was the case when Kubernetes, the orchestration tool that revolutionized container management, inundated the tech world, seemingly overnight.&lt;br&gt;
Amidst the buzz and fervor surrounding Kubernetes, I found myself in a curious position—I hadn't yet embarked on the journey to understand it.&lt;br&gt;
As the technology became ubiquitous, I recognized that the time had come to embrace this transformative platform and delve into its complexities.&lt;br&gt;
Thus began a new chapter in my technological voyage—a journey into the realm of Kubernetes, where each step forward unveils fresh challenges and endless opportunities for growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Kickoff: Assembling My Cluster, One Command at a Time
&lt;/h2&gt;

&lt;p&gt;As I delved into the world of Kubernetes, I opted for what seemed like the scenic route—until reality hit. With determination and a sprinkle of naivety, I plunged into crafting a Kubernetes cluster from scratch. Little did I know, I was in for a wild ride of commands, configurations, and complexities. But fear not! With the guiding light of a course based on the notorious &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;"Kubernetes The Hard Way" GitHub repository&lt;/a&gt;, albeit customized for AWS instead of Google Cloud, I embarked on my journey to build my digital fortress, one step at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing Our Cluster Blueprint: A Humble Beginning
&lt;/h3&gt;

&lt;p&gt;Voila! The Big Picture: Check Out the Cluster Diagram! Now, I'll admit, it's a bit like staring at a complex puzzle, but fear not, I am determined to piece it together! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhe59ofsv4rzohzzp77s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhe59ofsv4rzohzzp77s.png" alt="Cluster diagram" width="800" height="664"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigating the Kubernetes Landscape: A Condensed Journey
&lt;/h3&gt;

&lt;p&gt;Embarking on the Kubernetes setup journey felt like venturing into uncharted territory. From laying down prerequisites to fine-tuning configurations, each step presented its own challenges and insights. As I worked on setting up the Jumpbox, provisioning compute resources, and generating TLS certificates, I frequently consulted the official Kubernetes documentation for guidance. With each tool installed and resource provisioned, I delved deeper into Kubernetes intricacies, steadily building towards a functional cluster. From bootstrapping etcd to configuring kubectl for remote access, each action taught me something new, propelling me towards Kubernetes mastery. As the final smoke test cleared, a sense of accomplishment and excitement filled me, ready to explore the possibilities of my newly created Kubernetes cluster.&lt;br&gt;
Armed with the foundational knowledge gained from the setup journey, I now stand poised to embark on the next phase of my Kubernetes education&lt;/p&gt;

&lt;h2&gt;
  
  
  Embarking on my Kubernetes Mastery Journey
&lt;/h2&gt;

&lt;p&gt;Embarking on the path to Kubernetes expertise requires a structured approach, and the &lt;strong&gt;"Kubernetes Deep Dive"&lt;/strong&gt; course on A Cloud Guru serves as your trusty map. Let's take a quick tour of the course chapters to give you a glimpse of what lies ahead without bogging you down with details:&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 1: Course Introduction
&lt;/h3&gt;

&lt;p&gt;I received a warm welcome as I ventured into Chapter 1. Here, I was introduced to the course's objectives and got familiar with the prerequisites. The demo lessons were highlighted as key components enhancing my learning journey, setting the stage for the chapters to come.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 2: Kubernetes Big Picture
&lt;/h3&gt;

&lt;p&gt;In Chapter 2, I delved into the Kubernetes ecosystem, where the big picture started to take shape. Exploring the Kubernetes API, core objects, and receiving tips on setting up my own cluster laid a solid foundation for what was to follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 3: Application Architecture
&lt;/h3&gt;

&lt;p&gt;Transitioning into Chapter 3, I shifted my focus to application architecture within Kubernetes. Here, I not only explored the theoretical frameworks but also gained hands-on experience with a sample app, bridging the gap between theory and practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 4: Kubernetes Networking
&lt;/h3&gt;

&lt;p&gt;The spotlight then turned to networking in Chapter 4. Unraveling the mysteries of Kubernetes networking essentials, from understanding common requirements to practical demonstrations, provided me with a crucial foundation in cluster management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 5: Kubernetes Storage
&lt;/h3&gt;

&lt;p&gt;Opening the door to storage solutions in Chapter 5, I discovered the Container Storage Interface, persistent volumes, and storage classes. Through a blend of theory and hands-on exercises, these concepts became tangible building blocks in my journey.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 6: From Code to Kubernetes
&lt;/h3&gt;

&lt;p&gt;In Chapter 6, I learned to seamlessly deploy applications into Kubernetes environments. Following along with demos, I witnessed the transition from code to cluster, gaining practical insights into the deployment process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 7: Kubernetes Deployments
&lt;/h3&gt;

&lt;p&gt;As Kubernetes deployments took center stage in Chapter 7, I mastered the theory and practice of managing application lifecycles within Kubernetes clusters. This segment equipped me with the skills needed to ensure smooth operations from deployment to scaling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 8: Scaling Applications Automatically
&lt;/h3&gt;

&lt;p&gt;In Chapter 8, I was introduced to the art of automatic application scaling. Exploring strategies for horizontal to cluster autoscaling, I learned to adapt applications to varying workloads with ease.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chapter 9: RBAC and Admission Control
&lt;/h3&gt;

&lt;p&gt;Finally, Chapter 9 delved into Kubernetes security features, empowering me to secure my deployments effectively. Understanding role-based access control (RBAC) and admission control mechanisms fortified my Kubernetes deployments against potential threats.&lt;/p&gt;

&lt;p&gt;With this roadmap in hand, I embarked on my Kubernetes journey with confidence. Each chapter of the &lt;strong&gt;"Kubernetes Deep Dive"&lt;/strong&gt; course not only expanded my knowledge but also propelled me forward on my quest for Kubernetes mastery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting Knowledge into Practice: Following the EKS Immersion Day Workshop
&lt;/h2&gt;

&lt;p&gt;As I sought to apply my newly acquired Kubernetes knowledge in a practical setting, I turned to the cloud for real-world deployment experiences. One invaluable resource that caught my eye was &lt;a href="https://catalog.workshops.aws/eks-immersionday/en-US" rel="noopener noreferrer"&gt;the EKS Immersion Day workshop&lt;/a&gt;. This workshop not only provided a platform to apply my Kubernetes skills but also offered insights into AWS's unique approach to building clusters and managing containerized applications.&lt;/p&gt;

&lt;p&gt;This served as a bridge between theory and practice, allowing me to apply my knowledge in a cloud environment. What made this experience particularly enriching was AWS's distinctive approach to Kubernetes deployment.&lt;/p&gt;

&lt;p&gt;Throughout the workshop, I encountered AWS's nuances in cluster management, resource provisioning, and scalability. From leveraging AWS services like EKS (Elastic Kubernetes Service) to understanding how to optimize performance and cost efficiency, every step offered valuable insights into cloud-native practices.&lt;/p&gt;

&lt;p&gt;Moreover, the hands-on exercises and guided tutorials provided by the workshop enabled me to grasp AWS's methodologies effectively. I learned to navigate AWS's console, deploy applications using EKS, and troubleshoot common issues encountered in a cloud-based Kubernetes environment.&lt;/p&gt;

&lt;p&gt;Overall, the EKS Immersion Day workshop not only allowed me to apply my Kubernetes knowledge but also broadened my understanding of cloud-native architectures. It underscored the importance of adapting Kubernetes principles to different cloud providers' ecosystems, setting the stage for continued exploration and growth in my Kubernetes journey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts on My Kubernetes Journey
&lt;/h2&gt;

&lt;p&gt;Reflecting on my journey through Kubernetes, I'm struck by the growth and learning that have accompanied each step. From the initial setup of my cluster to the immersive experiences of workshops like the EKS Immersion Day, I've gained invaluable insights into container orchestration and cloud-native architectures.&lt;/p&gt;

&lt;p&gt;Looking ahead, I'm filled with excitement for the opportunities that lie on the horizon. While this milestone marks a significant achievement, it also serves as a reminder that the journey is far from over. With a wealth of knowledge and experiences at my disposal, I'm eager to continue exploring new technologies and pushing the boundaries of what's possible in the realm of Kubernetes.&lt;/p&gt;

&lt;p&gt;In closing, I'm grateful for the journey thus far and the lessons it has taught me. As I embark on the next phase of my Kubernetes journey, I do so with a sense of anticipation and a commitment to continued growth and exploration.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>learning</category>
    </item>
    <item>
      <title>AWS Summit Paris 2024</title>
      <dc:creator>Λ\: Laurent Noireterre</dc:creator>
      <pubDate>Fri, 12 Apr 2024 10:21:24 +0000</pubDate>
      <link>https://dev.to/stack-labs/aws-summit-paris-2024-458a</link>
      <guid>https://dev.to/stack-labs/aws-summit-paris-2024-458a</guid>
      <description>&lt;p&gt;&lt;em&gt;Article co-écrit avec Hicham Yahiaoui (Cloud Architect @Stack-Labs) et Yoann Metenier (Cloud Architect @Stack-Labs)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Keynote
&lt;/h2&gt;

&lt;h3&gt;
  
  
  La GenAI c’est génIAl
&lt;/h3&gt;

&lt;p&gt;L'AWS Summit Paris 2024 s’est déroulée au palais des congrès. Le lieu était plus que nécessaire au vu du nombre de personnes présentes à cet évènement. Dès notre arrivée, et après avoir récupéré nos cartes d’accès, nous nous sommes installés dans le grand amphithéâtre afin d’assister à la Keynote d’ouverture.&lt;/p&gt;

&lt;p&gt;Julien Groues (General Manager - Europe South, AWS) a effectué quelques présentations sur AWS en début de keynote pour ensuite laisser la main à Mai-Lan Tomsen-Bukovec (VP of Technology, AWS) qui est venu pour discuter des nouveautés 2024 AWS : &lt;strong&gt;la GenAI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Tout au long de cette keynote de 1h30 plusieurs intervenants sont montés sur scène afin de présenter leurs besoins et utilisations de la GenAI sur AWS. Cependant, une annonce surprise a été effectuée en début de session : &lt;strong&gt;AWS x Mistal AI&lt;/strong&gt;.&lt;br&gt;
En effet le premier intervenant sur scène n’est autre que Arthur Mensch CEO de Mistral AI. Il est venu pour confirmer son partenariat avec AWS permettant aux utilisateurs de disposer de Mistral AI dans AWS Bedrock pour la région Europe ! Les versions disponibles sont : Mistral Large, Mistral 7B et Mistral 8x7B.&lt;br&gt;
A la suite de cette annonce forte excitante, les intervenants suivants sont venu présenter leurs utilisation du cloud AWS pour dynamiser leur activité et enrichir l'expérience de leurs clients:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fabien Mangeant (Chief Data and AI Officer, Air Liquide)&lt;/li&gt;
&lt;li&gt;Thomas Wolf (Co-Founder, Hugging Face)&lt;/li&gt;
&lt;li&gt;Raphaëlle Deflesselle (CTO, Groupe TF1)&lt;/li&gt;
&lt;li&gt;Tom Brown (Co-Founder, Anthropic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A la fin de la keynote, nous avons poursuivi notre périple AWS en participant à quelques conférences parmi les 175 disponibles et en discutant sur les stands des  partenaires afin de découvrir des projets et utilisations de diverses solutions sur AWS. Nous avons choisi de vous présenter dans la suite de cet article 3 conférences auxquelles nous avons assisté.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-régions Zéro latence avec Kubernetes, Couchbase &amp;amp; Qovery
&lt;/h2&gt;

&lt;p&gt;Laurent Doguin (Developer Advocate Couchbase) et Romaric Philogène (CEO Qovery) nous ont fait le plaisir d’effectuer une présentation de l’intégration entre Couchbase et Qovery permettant de réduire et stabiliser une connexion BDD – Kubernetes dans un environnement multi-région sur AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Un peu de contexte
&lt;/h3&gt;

&lt;p&gt;Le temps d’attente de réponse d’une application peut provoquer une lassitude des utilisateurs d’autant plus à notre époque où nous sommes habitués à des services qui répondent rapidement. Nos interlocuteurs nous présentent les résultats d’une étude AWS qui dit :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 ms de latence sur la page amazon.com = 1% de baisse des ventes&lt;/li&gt;
&lt;li&gt;De manière générale : 

&lt;ul&gt;
&lt;li&gt;2 secondes de chargement pour un site internet = 9% des utilisateurs abandonneront la navigation,&lt;/li&gt;
&lt;li&gt;5 secondes de chargement pour un site internet = 38% des utilisateurs,&lt;/li&gt;
&lt;li&gt;3 secondes de chargement via smartphone = 53% des utilisateurs&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Le problème posé
&lt;/h3&gt;

&lt;p&gt;Dans une architecture multi-région comment puis-je faire pour disposer d’un temps de lecture/écriture acceptable pour n’importe quel client indépendamment de sa localisation géographique ?&lt;br&gt;
Nous pouvons représenter ce problème avec le schéma suivant :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbtej1o7wv7vp5afyi1c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbtej1o7wv7vp5afyi1c.png" alt="Image description" width="538" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nous pouvons voir que dans cette situation les client aux Etats-Unis et en Asie dispose d’un temps de lecture et écriture nettement supérieur à ceux en Europe. Comme expliqué dans notre contexte, ce délai supplémentaire peut provoquer de la frustration et donc amener à une perte d’utilisateurs sur ces régions.&lt;/p&gt;

&lt;p&gt;A la suite de cette mise en contexte, Laurent et Romaric ont proposé une solution de base souvent utilisée, appelée « active/passive ». Cette méthode consiste à déployer plusieurs instances d’application dans les différentes régions et d’utiliser des read réplicas pour les base de données (schéma ci-dessous).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa17kf29v910r8xzhz3un.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa17kf29v910r8xzhz3un.png" alt="Image description" width="591" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Via cette solution nous constatons une nette amélioration concernant la lecture du contenu en base. Si la lecture représente la plus grande partie des actions  réalisées par les clients, alors cette solution est viable. Mais quid de la situation où l'écriture est aussi un aspect important pour les clients ? Dans ce cas la solution basique devient non valide car nous disposons toujours d’un temps d’écriture très élevé pour les deux régions éloignées.&lt;/p&gt;

&lt;p&gt;Plusieurs solutions se présentent alors :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;     Retravailler le modèle de données pour séparer les régions entre-elles et que chacune repose sur sa propre base de données (beaucoup de travail à faire et peut-être pas possible en fonction du modèle de données)&lt;/li&gt;
&lt;li&gt;     Utiliser un schéma plus horizontale avec la possibilité d’écrire/lire les données de chaque base de données correspondant à sa région et gérer un système de réplication de données (complexe à mettre en œuvre et gestion des conflits sur les données à gérer soi-même)&lt;/li&gt;
&lt;li&gt;     Utiliser la solution « active/active » proposé par Couchbase et Qovery afin de permettre à la fois de disposer de bases de données par région et synchronisées entre elles de manière efficace via Couchbase, mais également de disposer du gestionnaire de déploiement serverless des ressources et plateformes centralisé, Qovery, connecté aux instances Couchbase&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Les présentateurs ont conclu leur conférence par la présentation de la solution (vous l’aurez deviné) N°3 : l’utilisation de Couchbase (Capella) et Qovery (schéma ci-dessous). Dans cette dernière, les utilisateurs de n’importe quelle région disposent tous du même temps de lecture et écriture sur l’application tout en bénéficiant d’une synchronisation des données avec latence faible complètement gérée par Couchbase. &lt;br&gt;
A noter également que la solution proposée par Couchbase permet de disposer des données de manière active/active tout en gérant l’aspect conformité de ces dernières (via l’utilisation de filtres) vis à vis des lois en vigueurs dans les régions et pays de déploiements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst929tk1634sa4p8efzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fst929tk1634sa4p8efzl.png" alt="Image description" width="616" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  En conclusion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Couchbase x Qovery&lt;/strong&gt; est un couple prometteur. En effet, la solution exige un coût supplémentaire par rapport à une solution gérée par le client lui-même. Cependant, aujourd’hui de nombreux clients souhaitent réduire l’aspect maintenance et opérabilité de leurs infrastructures sur le Cloud.&lt;br&gt;
Avec des interfaces claires et faciles d’utilisation (qui changent grandement de l’interface console AWS) la solution proposée peut être une alternative intéressante pour des clients avec un besoin spécifique et rapide avec une infrastructure simple.&lt;/p&gt;

&lt;p&gt;Vous pouvez retrouver une démo ici : &lt;a href="https://www.youtube.com/watch?v=nza3ldlPI7w" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=nza3ldlPI7w&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Optimisez les coûts et la mise à l'échelle d'EKS avec Karpenter
&lt;/h2&gt;

&lt;p&gt;Imane Zeroual (AWS), Sebastien Allamand (AWS) et Martinho Moreira (Voodoo) nous présente le projet &lt;strong&gt;Karpenter&lt;/strong&gt;, sa mise en place dans un cluster EKS et un retour d'expérience sur les bénéfices apportés par cette solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Maximiser l'Efficacité des Clusters Kubernetes avec Karpenter
&lt;/h3&gt;

&lt;p&gt;Kubernetes s'est imposé comme l'une des solutions les plus populaires pour la gestion des applications conteneurisées à grande échelle. Cependant, malgré ses avantages indéniables, Kubernetes peut présenter des défis en matière de gestion des ressources et d'optimisation des clusters. C'est là que Karpenter entre en jeu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qu'est-ce que Karpenter ?
&lt;/h3&gt;

&lt;p&gt;Karpenter est un projet open-source développé par AWS qui vise à optimiser les clusters Kubernetes en automatisant le dimensionnement des nœuds. Son objectif principal est de garantir que les ressources sont utilisées de manière efficace tout en maintenant les performances et la disponibilité des applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comment fonctionne Karpenter ?
&lt;/h3&gt;

&lt;p&gt;Karpenter s’installe dans le cluster Kubernetes en tant qu’opérateur et va remplacer le mécanisme d’autoscaling d’AWS pour provisionner les nœuds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbz2fru37hmt82otcgatt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbz2fru37hmt82otcgatt.jpg" alt="Image description" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Karpenter analyse les demandes de ressources des pods et les regroupe en fonction de leurs caractéristiques. En utilisant ces informations, il peut déterminer la meilleure façon de répartir les charges de travail sur les nœuds disponibles, et ainsi réduire le nombre de nœuds nécessaires.&lt;/p&gt;

&lt;p&gt;Par exemple, il peut regrouper plusieurs pods légers sur un seul nœud pour libérer des ressources sur d'autres nœuds et ainsi les supprimer du cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd802gaxksgr3zpybqnn5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd802gaxksgr3zpybqnn5.jpg" alt="Image description" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffolu9auln3ztz142kdle.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffolu9auln3ztz142kdle.jpg" alt="Image description" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Karpenter peut également s'intégrer avec des services cloud tels qu'AWS Spot Instances ou des instances de type Graviton, ce qui permet d'optimiser les coûts tout en maintenant les performances des applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo5rrulrh0m1ay3pecb3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmo5rrulrh0m1ay3pecb3.jpg" alt="Image description" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvcyiwq658brps52fpl9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flvcyiwq658brps52fpl9.jpg" alt="Image description" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Afin de paramétrer et utiliser au mieux Karpenter, Sébastien Allamand nous présente ensuite quelques outils et méthodes tels que les détections de Drift du dataplane ou l’analyse approfondie de la perturbation des nœuds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5qzspb68p0zkh9y7n3i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5qzspb68p0zkh9y7n3i.jpg" alt="Image description" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm33u17alzmrasxntj8k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpm33u17alzmrasxntj8k.jpg" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retour d'expérience
&lt;/h3&gt;

&lt;p&gt;Pour finir cette session, Martinho Moreira de chez Voodoo nous fait un retour d'expérience sur leur mise en place de Karpenter, l’architecture et les points de vigilance qu’ils en ont retiré.&lt;/p&gt;

&lt;p&gt;Les slides parlent d’eux même :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9qro9vnnftcuuay5mtm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9qro9vnnftcuuay5mtm.jpg" alt="Image description" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyatot34wfi1hzjtqvoj1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyatot34wfi1hzjtqvoj1.jpg" alt="Image description" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2For3o630axlahl4la1ybx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2For3o630axlahl4la1ybx.jpg" alt="Image description" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  En conclusion
&lt;/h3&gt;

&lt;p&gt;Cette conférence fut très intéressante pour une découverte de cet outil de plus en plus utilisé dans le cadre d’optimisations FinOps.&lt;br&gt;
La présentation a été accompagnée d’une démo qui nous a permis de rendre concret certains use cases, et de constater en live le fonctionnement de l’outil et les optimisations réalisées par Karpenter.&lt;br&gt;
La 2ème partie de la conférence a bien complété ce talk avec un retour d'expérience concret de la part de Voodoo. Ils ont ainsi pu nous partager factuellement les retombées en termes de bénéfice de l’outil, les étapes de migration et les pièges à éviter lors de la mise en place de Karpenter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Accelerate Gen AI with Amazon Bedrock and Snowflake:
&lt;/h2&gt;

&lt;p&gt;Nadir Djadi de Snowflake a présenté une approche accélérée de l'intelligence artificielle générative en utilisant Amazon Bedrock et Snowflake.&lt;br&gt;
Après une brève introduction sur le rôle de Snowflake, il a présenté une vue d'ensemble de la plateforme en mettant en avant trois points clés : L'IA accessible au quotidien sans expertise, le déploiement rapide d'applications avec personnalisation, la sécurité et la gouvernance des données garanties.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweewnjeva7kpronx3whd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweewnjeva7kpronx3whd.png" alt="Image description" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ensuite, nous avons exploré certaines fonctionnalités offertes par Snowflake avant de nous concentrer sur la partie Amazon Bedrock. Cette dernière offre une intégration transparente des modèles fondamentaux (FMs) de divers fournisseurs pour des applications d'intelligence artificielle générative évolutive, avec des options de personnalisation privées.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy88871p25xhb209md6h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwy88871p25xhb209md6h.png" alt="Image description" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nous avons ensuite passé en revue les FM disponibles sur Amazon Bedrock, en notant que Mistral n'a pas été inclus dans la liste puisqu'il a été annoncé le jour même. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fymf5uz2ey4scba5yoqa0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fymf5uz2ey4scba5yoqa0.png" alt="Image description" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enfin, il a conclu en montrant comment Snowflake peut interagir avec Amazon Bedrock via Snowpark External Access, qui repose sur des identifiants temporaires de AWS Security Token Service (STS) pour authentifier et accéder aux endpoint des modèles Amazon Bedrock.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3iyt7jcky5h1xd4aht0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo3iyt7jcky5h1xd4aht0.png" alt="Image description" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;En conclusion&lt;/strong&gt;, je trouve la solution présentée par Nadir Djadi très intéressante, surtout pour son aspect permettant une utilisation rapide de l'IA sans nécessiter une expertise préalable. Cela me donne vraiment envie d'expérimenter Amazon Bedrock dans mes projets futurs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Alors l’AWS Summit Paris c’est génIAl?
&lt;/h3&gt;

&lt;p&gt;Cette année encore AWS nous gratifie d’un show à la taille de son investissement en France. En effet, l’acteur n°1 du cloud public a réussi à nous faire sentir à l’étroit sur les 3 étages du Palais des congrès de Paris. Nous avons pu profiter un maximum, même si pour certaines conférences il était difficile d’avoir une place.&lt;/p&gt;

&lt;p&gt;Cette année s’annonce très intéressante sur le secteur de la GenAI. AWS compte bien rattraper son retard sur les aspects data et IA, en consacrant une attention particulière dans l’accompagnement de ses partenaires et clients voulant explorer ces solutions.&lt;/p&gt;

&lt;p&gt;Nous resterons bien sûr à l’écoute des nouveautés qu’AWS pourraient annoncer dans les mois à venir, et nous n'hésiterons pas à vous les partager sur notre blog.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>summit</category>
      <category>genai</category>
    </item>
    <item>
      <title>How to make Cloud Run talk to Cloud Run - The private way</title>
      <dc:creator>Kevin</dc:creator>
      <pubDate>Tue, 12 Mar 2024 13:30:56 +0000</pubDate>
      <link>https://dev.to/stack-labs/how-to-make-cloud-run-talk-to-cloud-run-the-private-way-2eb0</link>
      <guid>https://dev.to/stack-labs/how-to-make-cloud-run-talk-to-cloud-run-the-private-way-2eb0</guid>
      <description>&lt;p&gt;Cloud Run is no longer to be presented among the GCP container services. It is generally an efficient way to deploy web API applications without the overwhelming need of deploying and managing a Google Kubernetes Engine (GKE) cluster.&lt;/p&gt;

&lt;p&gt;When it comes to accessing a Cloud Run instance it is also really easily configurable to be accessed from the internet. But if you are in a production environment, or you just don't need your instance to be accessed from the outside, there are some private networking solutions.&lt;/p&gt;

&lt;p&gt;Like any other services, Cloud Run offers some out of the box features but some use cases may still require multiple options or services to be combined together.&lt;/p&gt;

&lt;p&gt;One of our clients that uses Cloud Run for internal purposes only, wanted one of his Cloud Run instances to access another one, still without going over the internet but using private internal paths.&lt;/p&gt;

&lt;p&gt;In this blog post, I will go over the Cloud Run networking options and details about how we finally did make a source Cloud Run service access a destination Cloud Run service using a private network.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Cloud Run can be accessed with authenticated or unauthenticated requests, this aspect will not be developed in this article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A bit of Cloud Run mechanics
&lt;/h2&gt;

&lt;p&gt;First of all, we could ask ourselves: why would it be tricky to make two Cloud Run instances communicate with each other?&lt;br&gt;
Whereas it is child's play to achieve it with two Compute Engine VM instances!&lt;/p&gt;

&lt;p&gt;Well, the main difference is that by default, Cloud Run is not a resource that is "attached" to a VPC the way a GCE instance is.&lt;/p&gt;

&lt;p&gt;You may know that every Cloud Run service deployment comes with a (ugly?) default URL that looks something like: &lt;code&gt;https://hello-g6bc2cfcrq-uc.a.run.app&lt;/code&gt;. Which is the only endpoint that is given by GCP when we create a service.&lt;/p&gt;

&lt;p&gt;As mentioned, one available option is to configure the instance to be accessible from the internet and use that URL from our source. But it is not what we want.&lt;/p&gt;

&lt;p&gt;At this point our puzzle looks like this : &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4y3fnlztvwyt80cg57n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4y3fnlztvwyt80cg57n.png" alt="Initial need" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, how is it possible to make a destination Cloud Run service to be accessed from private connections ? And a source Cloud Run service to access it ?&lt;/p&gt;

&lt;p&gt;Let's find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Run Ingress control
&lt;/h2&gt;

&lt;p&gt;On one hand we want to make our destination service accessible only from the internal network.&lt;/p&gt;

&lt;p&gt;Two options are available when it comes to configure a Cloud Run service inbound traffic:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allow ALL traffic&lt;/strong&gt;: which means our service is accessible from the internet.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restrict the access to "internal"&lt;/strong&gt; requests only, with the option to also allow requests from GCP external Load Balancers. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnmzzavv8l8q890ksd1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnmzzavv8l8q890ksd1k.png" alt="Cloud Run Ingress controls" width="558" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our case, the &lt;strong&gt;internal&lt;/strong&gt; option is the one that is used by our client to satisfy his requirements.&lt;/p&gt;

&lt;p&gt;Now we know that we only allow "internal" access to our &lt;em&gt;destination&lt;/em&gt; service, we need to find a way for our &lt;em&gt;source&lt;/em&gt; service to send requests through a VPC to be considered as internal ones.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslhdpjfg5cv2riqawk3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fslhdpjfg5cv2riqawk3o.png" alt="Internal ingress" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Run egress options
&lt;/h2&gt;

&lt;p&gt;The idea is to direct our source service traffic to the VPC, and for that Google has implemented Cloud Run "networking options":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outbound options&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;1. Serverless VPC Access Connector&lt;/strong&gt;: backstage this method creates a VM which is used to proxy requests from Cloud Run and forward them to the VPC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Direct Access&lt;/strong&gt;: this is the new option available since a few months in Cloud Run and still in "&lt;em&gt;Preview&lt;/em&gt;". Direct Access avoids the creation of additional resources (and so the associated costs), and is supposed to have more throughput, and lower latency.&lt;/p&gt;

&lt;p&gt;Be aware that when setting up any of those, you need to attach it to a VPC &lt;strong&gt;subnet&lt;/strong&gt;, and both will consume private IP addresses.&lt;/p&gt;

&lt;p&gt;Additionally we have to choose between two &lt;strong&gt;routing options&lt;/strong&gt; that will allow us to route our source service traffic in different ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Route &lt;strong&gt;only requests to private IPs&lt;/strong&gt; to the VPC&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Route &lt;strong&gt;all traffic&lt;/strong&gt; to the VPC&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Choosing either option will depend on some more parameters that we will see a bit later.&lt;/p&gt;

&lt;p&gt;That starts to make quite a few options combinations, but is that enough to access our destination service ? &lt;br&gt;
Not yet...&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Cloud Run endpoint
&lt;/h2&gt;

&lt;p&gt;We saw that by default, to access a Cloud Run service, the only endpoint is its (ugly!) URL (*.run.app).&lt;/p&gt;

&lt;p&gt;So we need either to be able to reach this URL from our VPC, or potentially to create a different endpoint.&lt;/p&gt;

&lt;p&gt;If using the default URL is ok for you, the first option you have is &lt;strong&gt;Private Google Access&lt;/strong&gt; (PGA). &lt;/p&gt;

&lt;p&gt;To make it simple Private Google Access is a one box option to check on your subnet configuration, and it enables resources with solely a private IP address to access GCP public APIs by staying inside the GCP's network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1pc1kvw1tgiptg3jc03e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1pc1kvw1tgiptg3jc03e.png" alt="Private Google Access" width="800" height="670"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: &lt;a href="https://cloud.google.com/vpc/docs/private-google-access#example" rel="noopener noreferrer"&gt;https://cloud.google.com/vpc/docs/private-google-access#example&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Magical isn't it ? Yes, but there are counterparts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;As you use the URL of your destination service, which is by default resolved as a public URL, you must configure the routing options of your source service to send "ALL TRAFFIC" to your VPC. That could be an issue if you want your Cloud Run instance to &lt;strong&gt;access&lt;/strong&gt; the internet and configured your it to use Direct VPC. Indeed, one Direct VPC limitation is that it is not compatible with Cloud NAT, which is the only way to give private instances a way to reach the internet. If your source service needs an internet access and you enable PGA, you have to use a Serverless VPC Access connector instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The IP address logged for the incoming request will be 0.0.0.0/32, so it could be difficult to do specific network tracing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You still use the ugly default URL to access your Cloud Run service.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What about the other options ?&lt;/strong&gt;&lt;br&gt;
Even if I will not go into the details, just know that if you have specific needs like accessing Cloud Run without directing all traffic to the VPC, or using an IP address or a custom DNS name, it is possible by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using an &lt;strong&gt;internal Load Balancer&lt;/strong&gt;, with the Cloud Run service as a backend&lt;/li&gt;
&lt;li&gt;Using a &lt;strong&gt;Private Service Connect&lt;/strong&gt; (PSC) endpoint with an associated Cloud DNS entry.&lt;/li&gt;
&lt;li&gt;Configure DNS to use the public URL as an internal endpoint so that it is routed to the VPC.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want me to detail those other options in a dedicated article, let me know in the comments section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our final configuration
&lt;/h2&gt;

&lt;p&gt;Putting all of those pieces together lead us with the following final solution for our client use case: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We used PGA and routed all traffic to the VPC. &lt;/li&gt;
&lt;li&gt;As the source Cloud Run instance needed an internet access, we used a VPC Access connector coupled with a NAT gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We finally came to this final configuration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs516h70p4h2uaundf2wu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs516h70p4h2uaundf2wu.png" alt="Final solution" width="800" height="793"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article and that helped you to understand a bit more about Cloud Run private networking.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;




&lt;p&gt;I’m Kevin, Cloud architect at &lt;a href="//www.stack-labs.com"&gt;Stack Labs&lt;/a&gt;.&lt;br&gt;
If you want to join an enthusiast Infra team, feel free to &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;contact us&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>cloudrun</category>
      <category>cloud</category>
      <category>network</category>
    </item>
    <item>
      <title>Déployons des canaris sur Google Kubernetes Engine</title>
      <dc:creator>Antoine Aubé</dc:creator>
      <pubDate>Tue, 13 Feb 2024 12:58:46 +0000</pubDate>
      <link>https://dev.to/stack-labs/deployons-des-canaris-sur-google-kubernetes-engine-4ka6</link>
      <guid>https://dev.to/stack-labs/deployons-des-canaris-sur-google-kubernetes-engine-4ka6</guid>
      <description>&lt;p&gt;Dans le cadre de mon emploi chez &lt;em&gt;Stack Labs&lt;/em&gt;, j'interviens auprès d'une entreprise qui découvre la conteneurisation et qui veut développer son nouveau système sur &lt;em&gt;Google Cloud Platform&lt;/em&gt;. L'un des sujets qui leur importe est la &lt;strong&gt;montée de version&lt;/strong&gt; : comment procéder ?&lt;br&gt;
Parmi leurs exigences, ils souhaitaient minimiser la durée d’interruption du service pendant la montée de version, ainsi que l'interrompre en cas de problème. &lt;br&gt;
Notre proposition : le &lt;strong&gt;déploiement en canari&lt;/strong&gt;. Dans ce billet, je vous présente (très) brièvement de quoi il s'agit, ainsi que la démonstration que j'ai préparée avec un collègue pour montrer à nos clients comment ça fonctionne.&lt;/p&gt;

&lt;h2&gt;
  
  
  Déploiement en canari ?
&lt;/h2&gt;

&lt;p&gt;Le déploiement en canari consiste, pour un serveur Web, à déployer la nouvelle version du serveur (le « &lt;em&gt;Canari&lt;/em&gt; ») en parallèle de l'ancienne (la « &lt;em&gt;version primaire&lt;/em&gt; »), et de diriger une partie du trafic sur cette nouvelle version. Si le &lt;em&gt;Canari&lt;/em&gt; est jugé conforme aux attentes pour les requêtes traitées, alors la portion du trafic qui lui est assignée augmente progressivement, jusqu'à arriver à un seuil critique : dès lors, la &lt;em&gt;version primaire&lt;/em&gt; est reconfigurée pour délivrer la nouvelle version, et tout le trafic lui est affecté ; quant au &lt;em&gt;Canari&lt;/em&gt;, les ressources qui lui étaient allouées (p. ex. conteneurs) sont libérées.&lt;/p&gt;

&lt;p&gt;Ci-suivent quelques liens pour des explications plus approfondies (et illustrées !) :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://martinfowler.com/bliki/CanaryRelease.html" rel="noopener noreferrer"&gt;Une explication par Danilo Sato, sur le site de Martin Fowler&lt;/a&gt; ;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://wa.aws.amazon.com/wellarchitected/2020-07-02T19-33-23/wat.concept.canary-deployment.en.html" rel="noopener noreferrer"&gt;Un résumé par &lt;em&gt;Amazon Web Services&lt;/em&gt;&lt;/a&gt; ;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codefresh.io/learn/software-deployment/what-are-canary-deployments/" rel="noopener noreferrer"&gt;Une explication par &lt;em&gt;CodeFresh&lt;/em&gt;&lt;/a&gt; ;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.encora.com/insights/zero-downtime-deployment-techniques-canary-deployments" rel="noopener noreferrer"&gt;Une explication par Isac Souza d’&lt;em&gt;Encora&lt;/em&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Après avoir lu ces pages Web, des questions apparaissent. En tout cas, je m'en suis posé plusieurs. Dans les sous-sections suivantes, je vous les présente, et vous explique mes réponses actuelles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Le &lt;em&gt;Canari&lt;/em&gt; est conforme... ?
&lt;/h3&gt;

&lt;p&gt;Déporter le trafic vers le &lt;em&gt;Canari&lt;/em&gt; a pour objectif de vérifier s'il est « conforme » ; certes, mais conforme à quoi ? &lt;/p&gt;

&lt;p&gt;Cela doit être défini par l'équipe d'exploitation.&lt;/p&gt;

&lt;p&gt;Un critère apparemment fréquent est le code de statut en réponse aux requêtes : par exemple, si un large pourcentage des réponses portent un code &lt;em&gt;HTTP 2xx&lt;/em&gt;, alors le déploiement peut se poursuivre, et il doit s'interrompre si les codes &lt;em&gt;HTTP 5xx&lt;/em&gt; sont majoritaires. Un autre critère est la latence de réponse : si elle est trop élevée, alors le déploiement doit s'interrompre.&lt;br&gt;
Ces critères sont intégrés nativement à des outils d'automatisation des déploiements en canari (p. ex. &lt;em&gt;Flagger&lt;/em&gt;), mais cela peut ne pas suffire : des critères orientés métier peuvent être plus pertinents pour valider la conformité du &lt;em&gt;Canari&lt;/em&gt;.&lt;br&gt;
Enfin, notons que ce type de déploiement n'est pas nécessairement automatisé. En effet, les opérateurs peuvent à chaque étape sélectionner une population de testeurs pour interagir avec le &lt;em&gt;Canari&lt;/em&gt;, et décider quand passer à la prochaine étape (p. ex. après avoir collecté les avis de la population de testeurs, ou mesuré diverses métriques avec des outils de supervision).&lt;/p&gt;

&lt;h3&gt;
  
  
  Déploiement d'un logiciel... ou de tout le système ?
&lt;/h3&gt;

&lt;p&gt;Les ressources trouvables en ligne ne sont pas claires quant à ce qu'englobe le déploiement en canari. En effet, si les auteurs parlent du déploiement d'un logiciel, leurs illustrations présentent bien le routage entre deux systèmes indépendants : l'un pour la &lt;em&gt;version primaire&lt;/em&gt;, l'autre pour le &lt;em&gt;Canari&lt;/em&gt;. Considérant les grandes difficultés techniques qui viennent avec la première option (p. ex. compatibilités entre modules, routage entre composants du système), il me semble plus raisonnable que ce soit la seconde option qui soit réellement mise en œuvre dans l'industrie.&lt;/p&gt;

&lt;p&gt;Cependant, cela ne vient pas sans défi.&lt;/p&gt;

&lt;p&gt;En effet, le déploiement en canari ne s'étend pas toujours à l'ensemble du système. Je prend l'exemple de l'une de mes expériences professionnelles passées : le système est déployé dans un environnement cloud qui compte un cluster &lt;em&gt;Kubernetes&lt;/em&gt; et un &lt;em&gt;SGBD&lt;/em&gt; déployé par un &lt;em&gt;PaaS&lt;/em&gt;. Dans ce contexte, le déploiement en canari ne concernait que les conteneurs déployés dans &lt;em&gt;Kubernetes&lt;/em&gt; : le contenu de la base de données était utilisé concurremment par la &lt;em&gt;version primaire&lt;/em&gt; et le &lt;em&gt;Canari&lt;/em&gt;. Par conséquent, il était important lorsque le &lt;em&gt;Canari&lt;/em&gt; modifiait le schéma de la base de données que les deux versions soient compatibles avec cette modification, au risque de perturber le fonctionnement du système lors du déploiement. Ce point d'attention est soulevé dans plusieurs des liens mentionnés plus haut, tel que l'article de Danilo Sato. C'est un exemple de contraintes sur le développement imposé par une pratique d'exploitation du système.&lt;/p&gt;

&lt;p&gt;D'autres préoccupations peuvent également survenir selon la nature des composants du système. Par exemple, l'explication d'&lt;em&gt;Encora&lt;/em&gt; indique qu'il faut être vigilant si certains composants possèdent un état (&lt;em&gt;stateful&lt;/em&gt;) : dès qu'un utilisateur a été dirigé sur l'une des deux versions, il faut qu'il y soit toujours dirigé au cours du déploiement ; et il peut être nécessaire de conserver l'ancienne version primaire quelque temps après la fin du déploiement le temps que les sessions soient fermées.&lt;/p&gt;

&lt;h2&gt;
  
  
  Démonstration
&lt;/h2&gt;

&lt;p&gt;Afin de présenter le fonctionnement d'un déploiement en canari à mon client, j'ai préparé une démonstration avec un collègue. Vous pouvez retrouver les fichiers de configuration et les instructions sur le &lt;a href="https://gitlab.com/stack-labs/oss/gke-canary-release" rel="noopener noreferrer"&gt;dépôt dans le groupe GitLab de Stack Labs&lt;/a&gt;.&lt;br&gt;
L’image ci-dessous (&lt;em&gt;très&lt;/em&gt; légèrement adaptée d’&lt;a href="https://docs.flagger.app/tutorials/nginx-progressive-delivery" rel="noopener noreferrer"&gt;une image de la documentation de &lt;em&gt;Flagger&lt;/em&gt;&lt;/a&gt;) illustre la démonstration.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1e3ai9ojksajng393pzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1e3ai9ojksajng393pzv.png" alt="Fonctionnement de Flagger dans Kubernetes avec NGINX" width="512" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nous disposons d'un serveur Web qui affiche une unique page avec un dessin de chameau. Nous le déployons dans &lt;em&gt;Google Kubernetes Engine&lt;/em&gt; (&lt;em&gt;GKE&lt;/em&gt;). En amont du serveur Web, nous déployons toujours dans &lt;em&gt;GKE&lt;/em&gt; un &lt;em&gt;Ingress Controller NGINX&lt;/em&gt; : il effectue le routage du trafic vers l'instance &lt;em&gt;primaire&lt;/em&gt; ou &lt;em&gt;Canari&lt;/em&gt; du serveur Web lors d'un déploiement.&lt;/p&gt;

&lt;p&gt;Nous voulons mettre à jour le serveur Web. En effet, alors que le fond de la page est beige (&lt;em&gt;version primaire&lt;/em&gt;), nous le voulons à présent vert (&lt;em&gt;Canari&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Pour parvenir à déployer la nouvelle version sans interruption du service, nous utilisons le logiciel &lt;a href="https://docs.flagger.app/" rel="noopener noreferrer"&gt;&lt;em&gt;Flagger&lt;/em&gt;&lt;/a&gt; qui automatise le processus de déploiement. Pour valider la conformité du &lt;em&gt;Canari&lt;/em&gt;, nous nous concentrons sur les codes de statut &lt;em&gt;HTTP&lt;/em&gt;, mais &lt;a href="https://docs.flagger.app/usage/metrics" rel="noopener noreferrer"&gt;&lt;em&gt;Flagger&lt;/em&gt; intègre d'autres critères nativement, et permet également de définir des critères personnalisés, grâce à des requêtes sur &lt;em&gt;Prometheus&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Dès le déploiement lancé, la version avec le fond beige cohabite avec celle au fond vert, et une portion croissante du trafic est dirigée vers cette dernière, comme expliqué précédemment.&lt;/p&gt;

&lt;p&gt;Les étapes de la démonstration sont détaillées sur le dépôt &lt;em&gt;Git&lt;/em&gt;, je vous invite à les parcourir et à réaliser vous-même la démonstration !&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Dans ce billet, je vous ai présenté une stratégie de déploiement en continu des logiciels : le déploiement en canari ; et je vous ai proposé quelques instructions pour le pratiquer par vous-même.&lt;/p&gt;

&lt;p&gt;Le déploiement en canari est une solution technique parmi d'autres pour déployer un système en continu. Je vous suggère ci-dessous trois lectures pour approfondir le sujet du déploiement en continu. D'abord, le &lt;a href="https://pub.fh-campuswien.ac.at/obvfcwhsacc/content/titleinfo/8874850/full.pdf" rel="noopener noreferrer"&gt;mémoire de Master de Nichil Strasser&lt;/a&gt;, qui évalue les outils de déploiement en canari sur le marché. Puis, deux recommandations qui ouvrent les perspectives sur d'autres techniques de déploiement : un &lt;a href="https://github.com/ContainerSolutions/k8s-deployment-strategies" rel="noopener noreferrer"&gt;dépôt &lt;em&gt;Git&lt;/em&gt; d'exemples sur &lt;em&gt;Kubernetes&lt;/em&gt;&lt;/a&gt;, et un &lt;a href="https://blog.ouidou.fr/modes-de-d%C3%A9ploiement-continu-rolling-upgrade-blue-green-canary-7d9abebd0806" rel="noopener noreferrer"&gt;article de Mahdi Konzali&lt;/a&gt; qui les vulgarise. Enfin, nous pouvons élargir l’exigence de faible temps d’interruption de service de notre client à la notion de haute disponibilité. Un &lt;a href="https://journalofcloudcomputing.springeropen.com/articles/10.1186/s13677-016-0066-8" rel="noopener noreferrer"&gt;article de Endo et coll.&lt;/a&gt;, publié en 2016, balaye plusieurs définitions de la haute disponibilité ainsi que l’état de l’art des moyens de sa mise en œuvre dans un contexte &lt;em&gt;cloud&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;Merci d’avoir lu mon article ! Je suis Antoine, ingénieur &lt;em&gt;cloud&lt;/em&gt; &amp;amp; doctorant chez &lt;em&gt;Stack Labs&lt;/em&gt;. Si vous voulez en savoir plus sur &lt;a href="http://stack-labs.com" rel="noopener noreferrer"&gt;&lt;em&gt;Stack Labs&lt;/em&gt;&lt;/a&gt; ou rejoindre une équipe de passionnés de tech, n’hésitez pas à nous contacter &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;ici&lt;/a&gt;. &lt;/p&gt;




&lt;p&gt;Crédits à Julia Craice sur &lt;em&gt;Unsplash&lt;/em&gt; pour l’image de couverture.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cd</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>Migration Rancher de Docker vers RKE2</title>
      <dc:creator>Julien RATON</dc:creator>
      <pubDate>Thu, 01 Feb 2024 14:04:27 +0000</pubDate>
      <link>https://dev.to/stack-labs/migration-rancher-de-docker-vers-rke2-2h4a</link>
      <guid>https://dev.to/stack-labs/migration-rancher-de-docker-vers-rke2-2h4a</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclaimer: le but de cet article n’est pas d’apporter une solution parfaite.Il est basé sur mon expérience pour répondre à un besoin client avec ses contraintes, mes connaissances et ma vision de la solution pour y répondre.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Qu’est-ce que Rancher ? Rancher est un outil qui permet de manager différents clusters Kubernetes, tout en fournissant des outils pour gérer des workflows conteneurisés.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhf9w1opjx67pgkmsw7j6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhf9w1opjx67pgkmsw7j6.png" title="test" alt="Représentation de 2 architectures possibles pour Rancher" width="800" height="484"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Représentation de 2 architectures possibles pour Rancher (Schéma issue de la documentation du &lt;a href="https://ranchermanager.docs.rancher.com/reference-guides/rancher-manager-architecture/architecture-recommendations#separation-of-rancher-and-user-clusters" rel="noopener noreferrer"&gt;site Rancher&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Le client chez qui je suis intervenu pour faire cette migration avait un Rancher qui tournait dans un container. Ils avaient comme besoin de rendre l’outil plus résilient et robuste. Effectivement Rancher était l’outil permettant de générer les accès aux clusters pour toute leur population de développeur. Cet outil était utilisé quotidiennement par plusieurs personnes pour déployer des environnements dans des clusters Kubernetes. La migration de Rancher vers un cluster Kubernetes (RKE2 ici) s’est imposée d'elle-même à la vue des différents enjeux et contraintes de ce client.&lt;/p&gt;

&lt;p&gt;Certains systèmes ou outils ont parfois besoin d’être migrés. Cependant tous n’ont pas été développés ou même pensés pour répondre à un tel besoin. C’est le cas de Rancher qui offre un outil de dump de ses données mais n’apporte pas de réelle solution à sa migration.&lt;br&gt;
Effectivement on peut trouver un outil sur le site de Rancher qui permet de faire une migration. Seulement cet outil apporte des contraintes qui n’étaient pas contournables lors de mon expérience. La première (et pas des moindres), le hostname doit rester le même. Cela peut paraître assez anodin, mais si on souhaite faire évoluer ce dernier, pour des contraintes réseaux, ou simplement que l’on ai envie de changer de hostname, l’outil fourni par Rancher n’est pas envisageable. La seconde contrainte était le fait de conserver l’environnement de départ. La migration peut s’effectuer de Docker vers Docker, de Kubernetes vers Kubernetes, mais pas de Docker vers Kubernetes ou inversement. &lt;/p&gt;

&lt;p&gt;Heureusement, le rancher propose un outil pour extraire les données liées à un cluster et également pour les importer. Cet outil est une CLI qui permet de contacter l’API de Rancher et d'interagir avec.&lt;br&gt;
Dans la suite de cet article je vous présente la façon dont j’ai pensé cette migration en ayant le moins d’indisponibilité possible pour les utilisateurs. Je pense que ce processus est fortement corrélé aux contraintes, au contexte et au besoin du projet sur lequel je suis intervenu. A vous de vous approprier la méthode plutôt que le processus lui-même.&lt;/p&gt;

&lt;p&gt;L’objectif était de créer le moins d’indisponibilité possible de la plateforme Rancher pour les utilisateurs. Il était donc nécessaire de bien préparer en amont de la bascule pour que cette dernière soit la plus rapide possible. La préparation commençait donc par extraire les ACLs (utilisateurs, rôles associés par projets et namespace). Pour cette partie là j’ai donc écrit un script Bash qui s’appuyait sur la CLI fournie par Rancher. Dans une boucle, le script récupère l’ensemble des projets, et pour chacun des projets, le(s) namespace(s) associé(s), avec les utilisateurs associés à leur(s) rôle(s). Ces informations, une fois récupérées, sont inscrites dans un fichier texte.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash


echo "clusters list"
clusters=$(rancher clusters ls | grep active | grep -v local | awk '{ print $1 }')
echo $clusters
echo ""


for cluster in $clusters
do
 cluster_name=$(rancher clusters ls | grep $cluster | awk '{ print $3 }')
 echo "project of current cluster - $cluster_name"
 projects=$(: | rancher context switch | grep -v local | grep -v 'Select a Project' | grep $cluster | awk '{ print $3 }')
 echo "cluster:$cluster_name" &amp;gt; $cluster_name.txt
 rancher clusters list-members --cluster-id $cluster | tail -n +2 | grep -v "Default Admin" | awk '{print $2 ":" $3}' &amp;gt;&amp;gt; $cluster_name.txt


 for project in $projects
 do
   rancher context switch $project
   project_name=$(: | rancher context switch | grep $project | awk '{ print $4 }')
   echo "members of current project - $project_name"
   echo "project:$project_name" &amp;gt;&amp;gt; $cluster_name.txt
   rancher namespaces ls -q | sed -E 's/(.*)/namespace:\1/' &amp;gt;&amp;gt; $cluster_name.txt
   rancher projects list-members --project-id $project | awk '{ print $2 ":" $3 }' | tail -n +2 &amp;gt;&amp;gt; $cluster_name.txt
 done
 echo ""
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Exemple de code d’export en bash&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La préparation à ce stade est terminée. L’étape suivante cause de l’indisponibilité, puisqu’il s’agit de la migration des agents. Avant de commencer, il faut créer le cluster dans le nouveau Rancher. Il suffit ensuite de récupérer la commande “kubectl apply” pour installer les nouveaux agents sur le cluster (et la garder de côté, la commande). Connectez-vous à votre cluster, et supprimez les anciens agents (vous pouvez supprimer le namespace, il sera recréé). Une fois supprimés, jouez la commande “kubectl apply” récupérée sur le nouveau Rancher.&lt;/p&gt;

&lt;p&gt;La bascule est maintenant terminée, il suffit donc de remapper l’ensemble des ACLs, que l’on a conservées dans un fichier texte avant la bascule. Pour ce faire, j’ai écrit un second script bash (qui s’appuie aussi sur la CLI fourni par Rancher). Dans ce dernier, il y a une boucle qui parcourt notre fichier texte pour récupérer les différentes informations et, à l’aide de la CLI Rancher, les injecte directement dans le nouveau Rancher.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash


imported_file=$1
echo "file $imported_file"
cluster_acls=$(cat $imported_file)
cluster=""
project=""


for cluster_acl in $cluster_acls
do
 echo $cluster_acl
 title=$(echo $cluster_acl | cut -d':' -f1)
 role=$(echo $cluster_acl | cut -d':' -f2)
 if [ "$title" = "cluster" ]
 then
   cluster=$role
   cluster_id=$(rancher clusters ls | grep $cluster | awk '{ print $1 }')
   echo "you are working on $cluster cluster with id $cluster_id"
 elif [ "$title" = "project" ]
 then
   project=$role
   project_id=$(: | rancher context switch | grep $cluster | grep $project | awk '{ print $3 }' | cut -d':' -f2)
   if [ "$project_id" = "" ]
   then
     rancher projects create --cluster $cluster_id $project
     project_id=$(: | rancher context switch | grep $cluster | grep $project | awk '{ print $3 }' | cut -d':' -f2)
   fi


   echo ""
   rancher context switch $cluster_id:$project_id
   echo "you are working on $project project with id $project_id"
 elif [ "$title" = "namespace" ]
 then
   namespace=$role
   rancher namespace move $namespace $cluster_id:$project_id
 else
   user=$title


   if [ "$project" = "" ]
   then
     current_user=$(rancher clusters list-members --cluster-id $cluster_id | grep $user | grep $role)


     if [ "$current_user" = "" ]
     then
       rancher clusters add-member-role --cluster-id $cluster_id $title $role
       echo "user $user added to the cluster $cluster with id $cluster_id with role $role"
     else
       echo "user $user with role $role already exists on current cluster $cluster"
     fi
   else
     current_user=$(rancher projects list-members | grep $user | grep $role)


     if [ "$current_user" = "" ]
     then
       rancher projects add-member-role $title $role
       echo "user $user added to the project $project with id $project_id with role $role"
     else
       echo "user $user with role $role already exists on current project $project"
     fi
   fi
 fi
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Exemple de code d’import en bash&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Il ne reste plus qu’à communiquer le nouveau DNS aux utilisateurs pour qu’ils récupèrent le nouveau contexte, le mettent en place sur leur espace de travail, et la migration est terminée !&lt;/p&gt;

&lt;p&gt;En conclusion: la solution Rancher apporte une solution simple pour manager ses clusters Kubernetes, mais ne semble pas être le moyen le plus efficace et le plus maintenable dans le temps. Cependant certaines facilités et outils qu’elle apporte peuvent faciliter certaines tâches (comme la délégation de droits sur les clusters par exemple).&lt;/p&gt;

&lt;p&gt;Concernant la migration, si vous vous retrouvez avec le même besoin et les mêmes contraintes, la solution est simple à mettre en place (quelques scripts bash, et de la configuration Rancher) et avec une rupture de service relativement courte.&lt;/p&gt;

&lt;p&gt;Merci d’avoir lu mon article! Je suis Julien, cloud engineer chez &lt;a href="http://stack-labs.com/" rel="noopener noreferrer"&gt;Stack Labs&lt;/a&gt;.&lt;br&gt;
Si vous voulez en savoir plus sur Stack Labs ou rejoindre une équipe de passionnés de tech, n’hésitez pas à nous contacter &lt;a href="https://www.welcometothejungle.com/fr/companies/stack-labs" rel="noopener noreferrer"&gt;ici&lt;/a&gt;. &lt;/p&gt;

</description>
      <category>rancher</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
