Ce n’est un secret pour personne : sans processus clairs et organisés, les développeurs peuvent avoir du mal à collaborer efficacement, ce qui entraîne des retards dans la livraison des mises à jour logicielles. Il y a quelques années, l'équipe du Social Discovery Group a été confrontée au défi d'un processus CI/CD sous-optimal. A cette époque, l’équipe utilisait TeamCity et Octopus, chacun avec ses atouts. Par exemple, Octopus est pratique pour les déploiements, tandis que TeamCity est bon pour les tests automatisés et suffisamment pratique pour la construction de projets. Pour construire un pipeline CI/CD complet et visuellement attrayant, dont la configuration est aussi pratique et flexible que possible, il est nécessaire d'utiliser une combinaison d'outils. Le code était stocké dans un référentiel local sur Bitbucket pour plusieurs projets. L'équipe SDG a étudié la problématique et a décidé d'optimiser le processus en utilisant les outils existants.
Objectifs d'optimisation clés :
L'équipe SDG a décidé d'utiliser TeamCity pour les builds et les tests automatisés, et Octopus pour les déploiements.
Ce qui a été implémenté dans TeamCity :
TeamCity permet l'utilisation de trois agents dans la version gratuite, ce qui était suffisant pour l'équipe SDG. Ils ont installé un nouvel agent, l'ont ajouté au pool et l'ont appliqué à leurs modèles.
Au moment d'utiliser la dernière version de TeamCity, l'équipe travaillait sur Ubuntu Server. La capture d'écran montre les plugins supplémentaires utilisés par l'équipe :
Le déploiement de NuGet ressemblait à ceci :
Il est à noter que selon que la branche est maître ou non, "-release" a été ajouté à la version (étapes 3, 4).
Le déploiement des services est visible ci-dessous :
Pour chaque service, les variables correspondantes ont été remplacées en fonction des variables système (nom du service, %build.number% et autres).
Un exemple de l'étape Docker Build est présenté dans la capture d'écran :
Chaque référentiel de projet contenait le Dockerfile correspondant.
Les différences entre les étapes 4 et 5, comme mentionné précédemment, étaient les suivantes :
La variable %deploymentTarget%
servait de paramètre Environnement(s), auquel les étapes correspondantes dans Octopus étaient passées lors du déploiement (par exemple, Test, Dev). Lorsque les modifications étaient transmises aux branches respectives (configurées) des équipes de développement, les builds et les déploiements de logiciels vers les environnements de test correspondants étaient automatiquement effectués. Les paramètres sont visibles dans la capture d'écran ci-dessous. Pour se connecter à Octopus, deux paramètres globaux devaient être ajoutés : octopus.apiKey et octopus.url
De plus, l'équipe SDG a connecté un référentiel NuGet commun et un Container Registry pour tous les projets de la section Connexions.
De plus, SDG recommande de configurer les notifications par e-mail dans la section Email Notifier, de configurer des sauvegardes dans la section Sauvegarde, de créer les groupes nécessaires, d'attribuer les rôles appropriés et d'ajouter des utilisateurs aux groupes requis. La configuration principale est terminée et, en conclusion, l'équipe recommande de vérifier régulièrement les mises à jour et de mettre à jour TeamCity une fois par mois.
Ensuite, l’équipe du Social Discovery Group est passée à la configuration d’Octopus. Cet article ne décrira pas les détails de l'installation, les paramètres de base des droits d'utilisateur et d'autres aspects, car vous pouvez facilement les faire vous-même. L'équipe a immédiatement abordé le cycle de vie, qui est configuré dans la section Bibliothèque. Dans la capture d'écran ci-dessous, vous pouvez voir un exemple de flux de l'équipe SDG :
Ensuite, l'équipe a créé tous les groupes de variables nécessaires par thèmes dans des ensembles de variables. Pour chaque variable, des valeurs ont été définies et des dépendances à l'environnement, des cibles et des rôles cibles (tags) ont été établies. Un exemple est présenté dans la capture d'écran ci-dessous :
Les clusters dans Kubernetes servaient de cibles et les rôles cibles étaient des balises attachées aux clusters ou environnements informatiques correspondants. Tout cela peut être configuré dans la section Infrastructure.
Les projets pourraient également être regroupés et un tableau de bord pratique pourrait être configuré pour afficher les services, les étapes et les versions déployées sur ceux-ci.
Le processus de déploiement de SDG s'est déroulé comme suit : toutes les étapes de test ont été combinées en une seule étape et un modèle commun a été créé pour elles, de la même manière pour la scène et les étapes en direct.
La capture d'écran ci-dessous montre à quoi cela ressemble pour l'équipe SDG :
À droite, le cycle de vie décrit précédemment a été sélectionné. L’étape Déployer un package comprenait des paramètres par défaut assez simples.
Pour l’étape Deploy Raw Kubernetes Yaml, l’équipe SDG a utilisé des modèles Yaml universels auto-écrits. Dans cet exemple, le script Kubernetes, est expliqué plus en détail ci-dessous. Les paramètres correspondants marqués en rouge ont également été remplacés. Il convient de noter que les groupes de variables globales nécessaires ont été connectés dans le menu Variables->Ensembles de variables et que les variables spécifiques au projet ont été définies dans le menu Variables->Projet, qui avait une priorité plus élevée.
Dans cet article, l'équipe SDG a décidé d'ignorer des détails tels que l'ajout d'un logo au projet, la configuration de déclencheurs ou d'autres détails mineurs. Concentrons-nous sur deux éléments de menu importants : 1 - Releases, où vous pouvez toujours voir la version et la date de création d'une version particulière ; ces informations sont également affichées sur le tableau de bord du projet, 2 - Variables->Aperçu, où vous pouvez voir quelles variables seront remplacées pour l'étape correspondante.
Passons à la partie la plus importante : le déploiement des modèles Yaml dans les clusters Kubernetes. Ils ont été créés dans la section Bibliothèque->Modèles d'étapes. Ci-dessous, l'équipe SDG a présenté une capture d'écran utilisant ses paramètres. Pour chaque paramètre, vous pouvez choisir une balise, un type et une valeur par défaut, ainsi qu'ajouter une description, ce qui est fortement recommandé.
Le code dans ce cas ressemblait à ceci :
apiVersion: apps/v1 kind: Deployment metadata: name: '#{Octopus.Project.Name | ToLower}' namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: replicas: #{Replicas} strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25% revisionHistoryLimit: 10 progressDeadlineSeconds: 600 selector: matchLabels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' template: metadata: labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: volumes: #{if usesidecar} - name: dump-storage persistentVolumeClaim: claimName: dumps-#{Octopus.Environment.Name | ToLower} #{/if} #{if MountFolders} #{each folder in MountFolders} - name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} hostPath: path: #{folder} type: DirectoryOrCreate #{/each} #{/if} - name: logs-volume hostPath: path: #{LogsDir} type: DirectoryOrCreate - name: appsettings secret: secretName: #{Octopus.Project.Name | ToLower} #{if Secrets} #{each secret in Secrets} - name: #{secret.name} secret: secretName: #{secret.name} #{/each} #{/if} #{if usesidecar} - name: diagnostics emptyDir: {} - name: dumps configMap: name: dumps defaultMode: 511 #{/if} containers: - name: #{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}-container image: #{DockerRegistry}/projectname.#{Octopus.Project.Name | ToLower}:#{Octopus.Release.Notes} #{if resources} resources: #{each resource in resources} #{resource.Key}: #{each entry in resource.Value} #{entry.Key}: #{entry.Value} #{/each} #{/each} #{/if} ports: - name: http containerPort: 80 protocol: TCP env: - value: "Development" name: "ASPNETCORE_ENVIRONMENT" - name: DD_ENV value: "#{Octopus.Environment.Name | ToLower}" - name: DD_SERVICE value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" - name: DD_VERSION value: "1.0.0" - name: DD_AGENT_HOST value: "#{DatadogAgentHost}" - name: DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED value: "true" - name: DD_RUNTIME_METRICS_ENABLED value: "true" volumeMounts: #{if usesidecar} - name: dump-storage mountPath: /tmp/dumps #{/if} #{if MountFolders} #{each folder in MountFolders} - mountPath: #{folder} name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower} #{/each} #{/if} - mountPath: #{LogsDir} name: logs-volume #{if usesidecar} - name: diagnostics mountPath: /tmp #{/if} - name: appsettings readOnly: true mountPath: /app/appsettings.json subPath: appsettings.json #{if Secrets} #{each secret in Secrets} - name: #{secret.name} readOnly: true mountPath: #{secret.mountPath} subPath: #{secret.subPath} #{/each} #{/if} readinessProbe: httpGet: path: hc port: http scheme: HTTP initialDelaySeconds: #{InitialDelaySeconds} imagePullPolicy: IfNotPresent securityContext: {} #{if usesidecar} - name: sidecar image: '#{DockerRegistry}/monitor:3' command: - /bin/sh args: - '-c' - while true; do . /app/init.sh; sleep 1m;done env: - name: USE_MEMORY value: '2048' - name: PROJECT value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}" resources: {} volumeMounts: - name: diagnostics mountPath: /tmp - name: dump-storage mountPath: /tmp/dumps - name: dumps mountPath: /app/init.sh subPath: init.sh shareProcessNamespace: true #{/if} affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: environment operator: In values: - "#{Node}" --- apiVersion: v1 kind: Service metadata: name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: type: ClusterIP selector: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' ports: - name: http port: 80 targetPort: http protocol: TCP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: ingress.kubernetes.io/ssl-redirect: 'false' nginx.ingress.kubernetes.io/ssl-redirect: 'false' cert-manager.io/cluster-issuer: "letsencrypt-cluster-issuer" cert-manager.io/renew-before: '#{LetsencryptRenewBefore}' kubernetes.io/ingress.class: nginx #{if IngressAnnotations} #{each annotation in IngressAnnotations} #{annotation.Key}: #{annotation.Value} #{/each} #{/if} name: #{Octopus.Project.Name | ToLower} namespace: #{Octopus.Environment.Name | ToLower} labels: Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}' spec: tls: #{if ExternalHost} #{each host in ExternalHost} - hosts: - #{host} secretName: #{Octopus.Project.Name | ToLower}-#{host | ToBase64 | Replace "\W" X | ToLower}-tls #{/each} #{/if} rules: #{if ExternalHost} #{each host in ExternalHost} - host: '#{host}' http: paths: - path: / pathType: ImplementationSpecific backend: service: name: #{Octopus.Project.Name | ToLower} port: name: http #{/each} #{/if} #{if usesidecar} --- apiVersion: v1 kind: ConfigMap metadata: name: dumps namespace: #{Octopus.Environment.Name | ToLower} data: init.sh: |- #!/usr/bin/env bash mem=$(ps aux | awk '{print $6}' | sort -rn | head -1) mb=$(($mem/1024)) archiveDumpPath="/tmp/dumps/$PROJECT-$(date +"%Y%m%d%H%M%S").zip" fullPathGc="/tmp/$PROJECT-$(date +"%Y%m%d%H%M%S").dump" echo "mem:" $mb" project:" $PROJECT "use:" $USE_MEMORY if [ "$mb" -gt "$USE_MEMORY" ]; then export USE_MEMORY=$(($USE_MEMORY*2)) pid=$(dotnet-dump ps | awk '{print $1}') dotnet-dump collect -p $pid -o $fullPathGc zip $fullPathGc.zip $fullPathGc mv $fullPathGc.zip $archiveDumpPath rm $fullPathGc fi #{/if}
Toutes les variables d'Octopus ont été spécifiées dans le format suivant dans le code : '#{Octopus.Project.Name | ToLower}'
, où la dernière partie indique la conversion en minuscules.
Le dernier fichier de configuration a été créé pour enregistrer automatiquement l'état des services .NET lorsqu'ils atteignaient une certaine limite d'utilisation de la mémoire. Cela a considérablement aidé à identifier les fuites de mémoire pendant le développement et à réparer rapidement les services.
Finalement, le tableau de bord du service ressemblait à ceci :
Les équipes de développement et de test ont trouvé très pratique de travailler avec ce tableau de bord.
Résultats d'optimisation :
Par la suite, SDG a implémenté de nombreuses autres fonctionnalités dans Octopus. Par exemple, arrêt automatique des clusters la nuit selon un planning.
Cependant, la recherche de la perfection ne connaît pas de limites. L’équipe de Social Discovery Group a avancé son développement en maîtrisant Azure DevOps. Ils ont mis en place un processus dans Azure DevOps au sein d'un écosystème sur Helm, encore plus complet et efficace. Cela sera abordé dans le prochain article.
Nous serions ravis de connaître votre expérience dans la configuration et l'optimisation de CI/CD à l'aide d'Octopus et TeamCity. Partagez vos idées et conseils !