[{"data":1,"prerenderedAt":1056},["ShallowReactive",2],{"blog-\u002Fblog\u002Freduire-couts-ci-runners-gitlab-spot":3,"blog-related-\u002Fblog\u002Freduire-couts-ci-runners-gitlab-spot":1037},{"id":4,"title":5,"body":6,"category":1021,"date":1022,"description":1023,"draft":1024,"extension":1025,"meta":1026,"navigation":118,"path":1027,"readingTime":122,"seo":1028,"sitemap":1029,"stem":1030,"tags":1031,"__hash__":1036},"blog\u002Fblog\u002Freduire-couts-ci-runners-gitlab-spot.md","Réduire le coût et la durée de vos pipelines GitLab avec des runners autoscalés",{"type":7,"value":8,"toc":1006},"minimark",[9,13,18,21,37,40,44,47,52,64,273,280,283,287,290,306,364,367,371,375,382,599,614,618,621,713,720,724,731,939,954,957,976,980,983,987,990,994,1002],[10,11,12],"p",{},"Une pipeline lente coûte deux fois : en factures d'infrastructure, et en temps d'attente des développeurs. Sur certaines missions, les optimisations décrites ci-dessous nous ont permis de gagner jusqu'à 40 % de temps de build, tout en réduisant la facture de calcul. Aucune n'est exotique : il s'agit surtout d'arrêter de payer pour du calcul inutilisé et de ne plus refaire ce qui a déjà été fait.",[14,15,17],"h2",{"id":16},"commencer-par-mesurer","Commencer par mesurer",[10,19,20],{},"Avant de modifier quoi que ce soit, établissez une base de référence. Sans elle, impossible de savoir si un changement aide.",[22,23,24,28,31,34],"ul",{},[25,26,27],"li",{},"La durée médiane et le 90e centile des pipelines de la branche principale.",[25,29,30],{},"Le temps passé en file d'attente avant le démarrage d'un job.",[25,32,33],{},"Les jobs les plus longs et les plus fréquents (ce sont eux qui paient).",[25,35,36],{},"Le coût de calcul mensuel des runners, ramené au nombre de pipelines.",[10,38,39],{},"GitLab expose ces données via les analyses CI\u002FCD et l'API. Notez-les avant, comparez après chaque changement, et ne cumulez pas dix modifications d'un coup : vous ne saurez plus laquelle a fonctionné.",[14,41,43],{"id":42},"des-runners-qui-sadaptent-à-la-charge","Des runners qui s'adaptent à la charge",[10,45,46],{},"Des machines allumées en permanence pour des pipelines qui tournent quelques heures par jour sont la première source de gaspillage. L'autoscaling règle le problème : les ressources n'existent que pendant les jobs.",[48,49,51],"h3",{"id":50},"runners-sur-kubernetes","Runners sur Kubernetes",[10,53,54,55,59,60,63],{},"L'exécuteur Kubernetes lance chaque job dans un pod éphémère. Combiné à un autoscaler de nœuds (Cluster Autoscaler ou Karpenter), le cluster grossit quand la file se remplit et se réduit à presque rien la nuit. Voici un extrait de ",[56,57,58],"code",{},"values.yaml"," pour le chart Helm ",[56,61,62],{},"gitlab-runner"," :",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-yaml shiki shiki-themes material-theme-lighter github-light github-dark","gitlabUrl: https:\u002F\u002Fgitlab.example.org\u002F\nconcurrent: 20\ncheckInterval: 3\n\nrbac:\n  create: true\n\nrunners:\n  secret: gitlab-runner-token\n  config: |\n    [[runners]]\n      [runners.kubernetes]\n        namespace = \"gitlab-runner\"\n        image = \"alpine:3.21\"\n        privileged = false\n        cpu_request = \"500m\"\n        memory_request = \"1Gi\"\n        cpu_limit = \"2\"\n        memory_limit = \"4Gi\"\n        [runners.kubernetes.node_selector]\n          \"workload\" = \"ci\"\n        [[runners.kubernetes.node_tolerations]]\n          key = \"ci\"\n          operator = \"Equal\"\n          value = \"true\"\n          effect = \"NoSchedule\"\n","yaml","",[56,72,73,90,102,113,120,129,141,146,154,165,177,183,189,195,201,207,213,219,225,231,237,243,249,255,261,267],{"__ignoreMap":70},[74,75,78,82,86],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"sQzsp","gitlabUrl",[74,83,85],{"class":84},"sP7_E",":",[74,87,89],{"class":88},"s_sjI"," https:\u002F\u002Fgitlab.example.org\u002F\n",[74,91,93,96,98],{"class":76,"line":92},2,[74,94,95],{"class":80},"concurrent",[74,97,85],{"class":84},[74,99,101],{"class":100},"srdBf"," 20\n",[74,103,105,108,110],{"class":76,"line":104},3,[74,106,107],{"class":80},"checkInterval",[74,109,85],{"class":84},[74,111,112],{"class":100}," 3\n",[74,114,116],{"class":76,"line":115},4,[74,117,119],{"emptyLinePlaceholder":118},true,"\n",[74,121,123,126],{"class":76,"line":122},5,[74,124,125],{"class":80},"rbac",[74,127,128],{"class":84},":\n",[74,130,132,135,137],{"class":76,"line":131},6,[74,133,134],{"class":80},"  create",[74,136,85],{"class":84},[74,138,140],{"class":139},"syTEX"," true\n",[74,142,144],{"class":76,"line":143},7,[74,145,119],{"emptyLinePlaceholder":118},[74,147,149,152],{"class":76,"line":148},8,[74,150,151],{"class":80},"runners",[74,153,128],{"class":84},[74,155,157,160,162],{"class":76,"line":156},9,[74,158,159],{"class":80},"  secret",[74,161,85],{"class":84},[74,163,164],{"class":88}," gitlab-runner-token\n",[74,166,168,171,173],{"class":76,"line":167},10,[74,169,170],{"class":80},"  config",[74,172,85],{"class":84},[74,174,176],{"class":175},"sVHd0"," |\n",[74,178,180],{"class":76,"line":179},11,[74,181,182],{"class":88},"    [[runners]]\n",[74,184,186],{"class":76,"line":185},12,[74,187,188],{"class":88},"      [runners.kubernetes]\n",[74,190,192],{"class":76,"line":191},13,[74,193,194],{"class":88},"        namespace = \"gitlab-runner\"\n",[74,196,198],{"class":76,"line":197},14,[74,199,200],{"class":88},"        image = \"alpine:3.21\"\n",[74,202,204],{"class":76,"line":203},15,[74,205,206],{"class":88},"        privileged = false\n",[74,208,210],{"class":76,"line":209},16,[74,211,212],{"class":88},"        cpu_request = \"500m\"\n",[74,214,216],{"class":76,"line":215},17,[74,217,218],{"class":88},"        memory_request = \"1Gi\"\n",[74,220,222],{"class":76,"line":221},18,[74,223,224],{"class":88},"        cpu_limit = \"2\"\n",[74,226,228],{"class":76,"line":227},19,[74,229,230],{"class":88},"        memory_limit = \"4Gi\"\n",[74,232,234],{"class":76,"line":233},20,[74,235,236],{"class":88},"        [runners.kubernetes.node_selector]\n",[74,238,240],{"class":76,"line":239},21,[74,241,242],{"class":88},"          \"workload\" = \"ci\"\n",[74,244,246],{"class":76,"line":245},22,[74,247,248],{"class":88},"        [[runners.kubernetes.node_tolerations]]\n",[74,250,252],{"class":76,"line":251},23,[74,253,254],{"class":88},"          key = \"ci\"\n",[74,256,258],{"class":76,"line":257},24,[74,259,260],{"class":88},"          operator = \"Equal\"\n",[74,262,264],{"class":76,"line":263},25,[74,265,266],{"class":88},"          value = \"true\"\n",[74,268,270],{"class":76,"line":269},26,[74,271,272],{"class":88},"          effect = \"NoSchedule\"\n",[10,274,275,276,279],{},"Le jeton du runner est lu depuis un secret Kubernetes, jamais écrit dans les values. Le ",[56,277,278],{},"node_selector"," et la tolérance dirigent les jobs vers un pool de nœuds dédié, facile à réduire à zéro. Définissez toujours des requests et des limits : sans elles, un job gourmand déstabilise ses voisins et l'autoscaler dimensionne mal.",[10,281,282],{},"Pour construire des images sans mode privilégié, préférez Kaniko, BuildKit en mode rootless ou Buildah plutôt que Docker-in-Docker.",[48,284,286],{"id":285},"instances-spot-ou-préemptibles","Instances spot ou préemptibles",[10,288,289],{},"Les instances spot (AWS), préemptibles (GCP) ou Spot VMs (Azure) coûtent nettement moins cher que le tarif à la demande, avec une contrepartie : le fournisseur peut les reprendre à tout moment. Les jobs de CI s'y prêtent bien, à condition de :",[22,291,292,300,303],{},[25,293,294,295,299],{},"les rendre ",[296,297,298],"strong",{},"idempotents",", donc relançables sans effet de bord ;",[25,301,302],{},"activer les relances automatiques ciblées sur les échecs d'infrastructure ;",[25,304,305],{},"garder un petit pool à la demande pour les jobs critiques, comme le déploiement en production.",[65,307,309],{"className":67,"code":308,"language":69,"meta":70,"style":70},"default:\n  retry:\n    max: 2\n    when:\n      - runner_system_failure\n      - stuck_or_timeout_failure\n      - scheduler_failure\n",[56,310,311,318,325,335,342,350,357],{"__ignoreMap":70},[74,312,313,316],{"class":76,"line":77},[74,314,315],{"class":80},"default",[74,317,128],{"class":84},[74,319,320,323],{"class":76,"line":92},[74,321,322],{"class":80},"  retry",[74,324,128],{"class":84},[74,326,327,330,332],{"class":76,"line":104},[74,328,329],{"class":80},"    max",[74,331,85],{"class":84},[74,333,334],{"class":100}," 2\n",[74,336,337,340],{"class":76,"line":115},[74,338,339],{"class":80},"    when",[74,341,128],{"class":84},[74,343,344,347],{"class":76,"line":122},[74,345,346],{"class":84},"      -",[74,348,349],{"class":88}," runner_system_failure\n",[74,351,352,354],{"class":76,"line":131},[74,353,346],{"class":84},[74,355,356],{"class":88}," stuck_or_timeout_failure\n",[74,358,359,361],{"class":76,"line":143},[74,360,346],{"class":84},[74,362,363],{"class":88}," scheduler_failure\n",[10,365,366],{},"On ne relance ainsi que les échecs dus à la plateforme, jamais les tests rouges.",[14,368,370],{"id":369},"ne-plus-rien-refaire-le-cache","Ne plus rien refaire : le cache",[48,372,374],{"id":373},"cache-des-dépendances-pnpm","Cache des dépendances pnpm",[10,376,377,378,381],{},"Le magasin de pnpm est un excellent candidat : il est adressé par contenu et partagé entre les projets. En le plaçant dans le cache GitLab, un ",[56,379,380],{},"pnpm install"," devient presque instantané.",[65,383,385],{"className":67,"code":384,"language":69,"meta":70,"style":70},"variables:\n  PNPM_STORE: \"$CI_PROJECT_DIR\u002F.pnpm-store\"\n\n.node-base:\n  image: node:22-alpine\n  before_script:\n    - corepack enable\n    - pnpm config set store-dir \"$PNPM_STORE\"\n  cache:\n    key:\n      files:\n        - pnpm-lock.yaml\n    paths:\n      - .pnpm-store\n    policy: pull\n\ninstall-deps:\n  extends: .node-base\n  stage: .pre\n  script:\n    - pnpm install --frozen-lockfile\n  cache:\n    key:\n      files:\n        - pnpm-lock.yaml\n    paths:\n      - .pnpm-store\n    policy: pull-push\n",[56,386,387,394,411,415,422,432,439,447,454,461,468,475,483,490,497,507,511,518,528,538,545,552,558,564,570,576,582,589],{"__ignoreMap":70},[74,388,389,392],{"class":76,"line":77},[74,390,391],{"class":80},"variables",[74,393,128],{"class":84},[74,395,396,399,401,405,408],{"class":76,"line":92},[74,397,398],{"class":80},"  PNPM_STORE",[74,400,85],{"class":84},[74,402,404],{"class":403},"sjJ54"," \"",[74,406,407],{"class":88},"$CI_PROJECT_DIR\u002F.pnpm-store",[74,409,410],{"class":403},"\"\n",[74,412,413],{"class":76,"line":104},[74,414,119],{"emptyLinePlaceholder":118},[74,416,417,420],{"class":76,"line":115},[74,418,419],{"class":80},".node-base",[74,421,128],{"class":84},[74,423,424,427,429],{"class":76,"line":122},[74,425,426],{"class":80},"  image",[74,428,85],{"class":84},[74,430,431],{"class":88}," node:22-alpine\n",[74,433,434,437],{"class":76,"line":131},[74,435,436],{"class":80},"  before_script",[74,438,128],{"class":84},[74,440,441,444],{"class":76,"line":143},[74,442,443],{"class":84},"    -",[74,445,446],{"class":88}," corepack enable\n",[74,448,449,451],{"class":76,"line":148},[74,450,443],{"class":84},[74,452,453],{"class":88}," pnpm config set store-dir \"$PNPM_STORE\"\n",[74,455,456,459],{"class":76,"line":156},[74,457,458],{"class":80},"  cache",[74,460,128],{"class":84},[74,462,463,466],{"class":76,"line":167},[74,464,465],{"class":80},"    key",[74,467,128],{"class":84},[74,469,470,473],{"class":76,"line":179},[74,471,472],{"class":80},"      files",[74,474,128],{"class":84},[74,476,477,480],{"class":76,"line":185},[74,478,479],{"class":84},"        -",[74,481,482],{"class":88}," pnpm-lock.yaml\n",[74,484,485,488],{"class":76,"line":191},[74,486,487],{"class":80},"    paths",[74,489,128],{"class":84},[74,491,492,494],{"class":76,"line":197},[74,493,346],{"class":84},[74,495,496],{"class":88}," .pnpm-store\n",[74,498,499,502,504],{"class":76,"line":203},[74,500,501],{"class":80},"    policy",[74,503,85],{"class":84},[74,505,506],{"class":88}," pull\n",[74,508,509],{"class":76,"line":209},[74,510,119],{"emptyLinePlaceholder":118},[74,512,513,516],{"class":76,"line":215},[74,514,515],{"class":80},"install-deps",[74,517,128],{"class":84},[74,519,520,523,525],{"class":76,"line":221},[74,521,522],{"class":80},"  extends",[74,524,85],{"class":84},[74,526,527],{"class":88}," .node-base\n",[74,529,530,533,535],{"class":76,"line":227},[74,531,532],{"class":80},"  stage",[74,534,85],{"class":84},[74,536,537],{"class":88}," .pre\n",[74,539,540,543],{"class":76,"line":233},[74,541,542],{"class":80},"  script",[74,544,128],{"class":84},[74,546,547,549],{"class":76,"line":239},[74,548,443],{"class":84},[74,550,551],{"class":88}," pnpm install --frozen-lockfile\n",[74,553,554,556],{"class":76,"line":245},[74,555,458],{"class":80},[74,557,128],{"class":84},[74,559,560,562],{"class":76,"line":251},[74,561,465],{"class":80},[74,563,128],{"class":84},[74,565,566,568],{"class":76,"line":257},[74,567,472],{"class":80},[74,569,128],{"class":84},[74,571,572,574],{"class":76,"line":263},[74,573,479],{"class":84},[74,575,482],{"class":88},[74,577,578,580],{"class":76,"line":269},[74,579,487],{"class":80},[74,581,128],{"class":84},[74,583,585,587],{"class":76,"line":584},27,[74,586,346],{"class":84},[74,588,496],{"class":88},[74,590,592,594,596],{"class":76,"line":591},28,[74,593,501],{"class":80},[74,595,85],{"class":84},[74,597,598],{"class":88}," pull-push\n",[10,600,601,602,605,606,609,610,613],{},"La clé de cache dérive du fichier ",[56,603,604],{},"pnpm-lock.yaml"," : le cache n'est renouvelé que si les dépendances changent. Un seul job écrit (",[56,607,608],{},"pull-push","), tous les autres se contentent de lire (",[56,611,612],{},"pull","), ce qui évite les écritures concurrentes.",[48,615,617],{"id":616},"cache-des-layers-docker","Cache des layers Docker",[10,619,620],{},"Pour les builds d'images, un registre de cache évite de reconstruire les couches inchangées :",[65,622,626],{"className":623,"code":624,"language":625,"meta":70,"style":70},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","docker buildx build \\\n  --cache-from type=registry,ref=\"$CI_REGISTRY_IMAGE:buildcache\" \\\n  --cache-to type=registry,ref=\"$CI_REGISTRY_IMAGE:buildcache\",mode=max \\\n  --tag \"$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA\" \\\n  --push .\n","bash",[56,627,628,644,667,687,705],{"__ignoreMap":70},[74,629,630,634,637,640],{"class":76,"line":77},[74,631,633],{"class":632},"sbgvK","docker",[74,635,636],{"class":88}," buildx",[74,638,639],{"class":88}," build",[74,641,643],{"class":642},"s_hVV"," \\\n",[74,645,646,650,653,656,660,663,665],{"class":76,"line":92},[74,647,649],{"class":648},"stzsN","  --cache-from",[74,651,652],{"class":88}," type=registry,ref=",[74,654,655],{"class":403},"\"",[74,657,659],{"class":658},"su5hD","$CI_REGISTRY_IMAGE",[74,661,662],{"class":88},":buildcache",[74,664,655],{"class":403},[74,666,643],{"class":642},[74,668,669,672,674,676,678,680,682,685],{"class":76,"line":104},[74,670,671],{"class":648},"  --cache-to",[74,673,652],{"class":88},[74,675,655],{"class":403},[74,677,659],{"class":658},[74,679,662],{"class":88},[74,681,655],{"class":403},[74,683,684],{"class":88},",mode=max",[74,686,643],{"class":642},[74,688,689,692,694,696,698,701,703],{"class":76,"line":115},[74,690,691],{"class":648},"  --tag",[74,693,404],{"class":403},[74,695,659],{"class":658},[74,697,85],{"class":88},[74,699,700],{"class":658},"$CI_COMMIT_SHORT_SHA",[74,702,655],{"class":403},[74,704,643],{"class":642},[74,706,707,710],{"class":76,"line":122},[74,708,709],{"class":648},"  --push",[74,711,712],{"class":88}," .\n",[10,714,715,716,719],{},"Ce cache n'est efficace que si le Dockerfile est bien ordonné : installer les dépendances ",[296,717,718],{},"avant"," de copier le code source, pour que la couche lourde ne soit invalidée que lorsque le lockfile change.",[14,721,723],{"id":722},"paralléliser-avec-needs-et-cibler-avec-rules","Paralléliser avec needs et cibler avec rules",[10,725,726,727,730],{},"Par défaut, GitLab attend la fin de tous les jobs d'une étape avant de passer à la suivante. Le mot-clé ",[56,728,729],{},"needs"," construit un graphe de dépendances : un job démarre dès que ceux dont il dépend sont terminés.",[65,732,734],{"className":67,"code":733,"language":69,"meta":70,"style":70},"lint:\n  extends: .node-base\n  stage: test\n  needs: [\"install-deps\"]\n  script: pnpm lint\n\nunit-tests:\n  extends: .node-base\n  stage: test\n  needs: [\"install-deps\"]\n  parallel: 4\n  script: pnpm test --shard=$CI_NODE_INDEX\u002F$CI_NODE_TOTAL\n\nbuild:\n  extends: .node-base\n  stage: build\n  needs: [\"install-deps\"]\n  script: pnpm build\n  rules:\n    - changes:\n        - src\u002F**\u002F*\n        - package.json\n        - pnpm-lock.yaml\n",[56,735,736,743,751,760,779,788,792,799,807,815,831,841,850,854,861,869,878,894,903,910,919,926,933],{"__ignoreMap":70},[74,737,738,741],{"class":76,"line":77},[74,739,740],{"class":80},"lint",[74,742,128],{"class":84},[74,744,745,747,749],{"class":76,"line":92},[74,746,522],{"class":80},[74,748,85],{"class":84},[74,750,527],{"class":88},[74,752,753,755,757],{"class":76,"line":104},[74,754,532],{"class":80},[74,756,85],{"class":84},[74,758,759],{"class":88}," test\n",[74,761,762,765,767,770,772,774,776],{"class":76,"line":115},[74,763,764],{"class":80},"  needs",[74,766,85],{"class":84},[74,768,769],{"class":84}," [",[74,771,655],{"class":403},[74,773,515],{"class":88},[74,775,655],{"class":403},[74,777,778],{"class":84},"]\n",[74,780,781,783,785],{"class":76,"line":122},[74,782,542],{"class":80},[74,784,85],{"class":84},[74,786,787],{"class":88}," pnpm lint\n",[74,789,790],{"class":76,"line":131},[74,791,119],{"emptyLinePlaceholder":118},[74,793,794,797],{"class":76,"line":143},[74,795,796],{"class":80},"unit-tests",[74,798,128],{"class":84},[74,800,801,803,805],{"class":76,"line":148},[74,802,522],{"class":80},[74,804,85],{"class":84},[74,806,527],{"class":88},[74,808,809,811,813],{"class":76,"line":156},[74,810,532],{"class":80},[74,812,85],{"class":84},[74,814,759],{"class":88},[74,816,817,819,821,823,825,827,829],{"class":76,"line":167},[74,818,764],{"class":80},[74,820,85],{"class":84},[74,822,769],{"class":84},[74,824,655],{"class":403},[74,826,515],{"class":88},[74,828,655],{"class":403},[74,830,778],{"class":84},[74,832,833,836,838],{"class":76,"line":179},[74,834,835],{"class":80},"  parallel",[74,837,85],{"class":84},[74,839,840],{"class":100}," 4\n",[74,842,843,845,847],{"class":76,"line":185},[74,844,542],{"class":80},[74,846,85],{"class":84},[74,848,849],{"class":88}," pnpm test --shard=$CI_NODE_INDEX\u002F$CI_NODE_TOTAL\n",[74,851,852],{"class":76,"line":191},[74,853,119],{"emptyLinePlaceholder":118},[74,855,856,859],{"class":76,"line":197},[74,857,858],{"class":80},"build",[74,860,128],{"class":84},[74,862,863,865,867],{"class":76,"line":203},[74,864,522],{"class":80},[74,866,85],{"class":84},[74,868,527],{"class":88},[74,870,871,873,875],{"class":76,"line":209},[74,872,532],{"class":80},[74,874,85],{"class":84},[74,876,877],{"class":88}," build\n",[74,879,880,882,884,886,888,890,892],{"class":76,"line":215},[74,881,764],{"class":80},[74,883,85],{"class":84},[74,885,769],{"class":84},[74,887,655],{"class":403},[74,889,515],{"class":88},[74,891,655],{"class":403},[74,893,778],{"class":84},[74,895,896,898,900],{"class":76,"line":221},[74,897,542],{"class":80},[74,899,85],{"class":84},[74,901,902],{"class":88}," pnpm build\n",[74,904,905,908],{"class":76,"line":227},[74,906,907],{"class":80},"  rules",[74,909,128],{"class":84},[74,911,912,914,917],{"class":76,"line":233},[74,913,443],{"class":84},[74,915,916],{"class":80}," changes",[74,918,128],{"class":84},[74,920,921,923],{"class":76,"line":239},[74,922,479],{"class":84},[74,924,925],{"class":88}," src\u002F**\u002F*\n",[74,927,928,930],{"class":76,"line":245},[74,929,479],{"class":84},[74,931,932],{"class":88}," package.json\n",[74,934,935,937],{"class":76,"line":251},[74,936,479],{"class":84},[74,938,482],{"class":88},[10,940,941,942,945,946,949,950,953],{},"Ici, lint, tests et build s'exécutent en même temps, et les tests sont répartis en quatre tranches avec ",[56,943,944],{},"parallel",". Les ",[56,947,948],{},"rules"," avec ",[56,951,952],{},"changes"," évitent de lancer un build quand seule la documentation a été modifiée. Dans un monorepo, c'est souvent le gain le plus spectaculaire.",[10,955,956],{},"Quelques règles complémentaires :",[22,958,959,966,969],{},[25,960,961,962,965],{},"annuler automatiquement les pipelines redondants avec ",[56,963,964],{},"interruptible: true"," ;",[25,967,968],{},"ne pas lancer les jobs lourds (e2e, scans complets) sur chaque commit de branche, mais en merge request ou de façon planifiée ;",[25,970,971,972,975],{},"éviter les pipelines doublons entre branche et merge request avec ",[56,973,974],{},"workflow:rules",".",[14,977,979],{"id":978},"des-images-légères","Des images légères",[10,981,982],{},"Une image de 1,5 Go se télécharge à chaque job sur un runner éphémère. Passer à des variantes Alpine ou « slim », construire une image d'outillage dédiée avec uniquement ce qui sert, et héberger ces images dans un registre proche des runners ou derrière un proxy de cache réduit sensiblement le temps de démarrage. C'est un gain discret mais multiplié par chaque job de chaque pipeline.",[14,984,986],{"id":985},"vérifier-le-résultat","Vérifier le résultat",[10,988,989],{},"Après chaque changement, comparez à votre base de référence : durée médiane, temps d'attente, coût par pipeline. Gardez ce qui fonctionne, retirez ce qui complexifie sans rapporter. L'optimisation de la CI est un travail continu, pas un chantier ponctuel.",[14,991,993],{"id":992},"besoin-dun-coup-de-main","Besoin d'un coup de main ?",[10,995,996,997,975],{},"Dimensionner des runners autoscalés, fiabiliser l'usage du spot ou auditer vos pipelines pour identifier les gains les plus rentables fait partie de nos missions habituelles. Pour en parler, vous pouvez ",[998,999,1001],"a",{"href":1000},"\u002F#contact","nous contacter",[1003,1004,1005],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sQzsp, html code.shiki .sQzsp{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}",{"title":70,"searchDepth":92,"depth":92,"links":1007},[1008,1009,1013,1017,1018,1019,1020],{"id":16,"depth":92,"text":17},{"id":42,"depth":92,"text":43,"children":1010},[1011,1012],{"id":50,"depth":104,"text":51},{"id":285,"depth":104,"text":286},{"id":369,"depth":92,"text":370,"children":1014},[1015,1016],{"id":373,"depth":104,"text":374},{"id":616,"depth":104,"text":617},{"id":722,"depth":92,"text":723},{"id":978,"depth":92,"text":979},{"id":985,"depth":92,"text":986},{"id":992,"depth":92,"text":993},"Infrastructure","2026-07-15","Runners Kubernetes ou spot, cache pnpm et Docker, jobs parallèles avec needs, rules ciblées : réduisez la facture et la durée de vos pipelines GitLab CI.",false,"md",{},"\u002Fblog\u002Freduire-couts-ci-runners-gitlab-spot",{"title":5,"description":1023},{"loc":1027,"lastmod":1022},"blog\u002Freduire-couts-ci-runners-gitlab-spot",[1032,1033,1034,1035],"gitlab","ci-cd","kubernetes","optimisation","FGKtsknOYamaq84NdIvlUiyeqdW_QBeTuZoRaglUjgw",[1038,1044,1050],{"path":1039,"title":1040,"description":1041,"date":1042,"category":1043,"readingTime":143},"\u002Fblog\u002Fapplication-mobile-pwa-native-tauri","Application mobile : native, Flutter, PWA ou Tauri 2, comment choisir ?","Native, cross-platform, PWA ou Tauri 2 : comparatif concret selon l'accès matériel, le hors-ligne, la maintenance et la publication sur les stores.","2026-10-06","Mobile",{"path":1045,"title":1046,"description":1047,"date":1048,"category":1049,"readingTime":131},"\u002Fblog\u002Fsecuriser-pipeline-gitlab-ci-devsecops","Sécuriser une pipeline GitLab CI\u002FCD sans ralentir vos livraisons","SAST, scan de dépendances et d'images, secrets, SBOM, signature cosign : comment intégrer la sécurité dans GitLab CI\u002FCD sans freiner vos mises en production.","2026-09-22","DevSecOps",{"path":1051,"title":1052,"description":1053,"date":1054,"category":1055,"readingTime":131},"\u002Fblog\u002Fseo-technique-nuxt","SEO technique avec Nuxt : le guide pour un site rapide et bien référencé","Rendu SSR ou pré-rendu, useSeoMeta, canoniques, sitemap, JSON-LD, Core Web Vitals : les réglages SEO techniques à maîtriser sur un site Nuxt 4.","2026-09-10","SEO",1792329028563]