Ajouter un mode « maintenance » à votre API grâce à Spring boot

Photo by Pixabay on Pexels.com

Quand vous avez une API, et a fortiori une application, il peut être parfois nécessaire de passer l’application en mode « maintenance ».
Pour certaines applications il est parfois inutile de le traiter au niveau applicatif, car ça peut être pris géré par certaines couches de sécurité ou frontaux web par ex. (Apache HTTPD, WAF,…)

Kubernetes a introduit ( ou popularisé ) les notions de « probes » et plus particulièrement les livenessProbes et readinessProbes.
Le premier nous indique si l’application est en état de fonctionnement, le second nous permet de savoir si cette dernière est apte à recevoir des requêtes (ex. lors d’un démarrage).

Je vais exposer dans cet article comment utiliser au mieux ces probes et les APIs SPRING pour intégrer dans une API un mode « maintenance »

Stack utilisée

Dans l’exemple que j’ai développé, j’ai pu utiliser les briques suivantes:

  • OpenJDK 11.0.10
  • Spring Boot 2.5.0 (web, actuator)
  • Maven 3.8.1

Bref, rien de neuf à l’horizon 🙂

Configuration de Spring Actuator

Pour activer les différents probes, vous devez activer Actuator.

Dans le fichier pom.xml, vous devez ajouter le starter correspondant:

<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Puis vous devez déclarer ces differentes propriétés:

management.endpoints.enabled-by-default=true
management.health.livenessstate.enabled=true
management.health.readinessstate.enabled=true
management.endpoint.health.show-details=always
management.endpoint.health.probes.enabled=true
management.endpoint.health.enabled=true

Après avoir redémarré votre application, vous pourrez connaître son statut grâce à un appel HTTP

curl -s http://localhost:8080/actuator/health/readiness 

Comment récupérer le statut des probes?

Avec Spring, vous pouvez modifier les différents statuts avec les classes ApplicationEventPublisher et ApplicationAvailability.

Par exemple, pour connaître le statut "Readiness" vous pouvez exécuter le code suivant:

 @ApiResponses(value = {
 @ApiResponse(responseCode = "200", description = "Checks if the application in under maitenance")})
 @GetMapping
 public ResponseEntity<MaintenanceDTO> retreiveInMaintenance() {
        var lastChangeEvent = availability.getLastChangeEvent(ReadinessState.class);
        return ResponseEntity.ok(new MaintenanceDTO(lastChangeEvent.getState().equals(ReadinessState.REFUSING_TRAFFIC), new Date(lastChangeEvent.getTimestamp())));
    }

Et la modification ?

Grâce à la même API, on peut également modifier ce statut dans via du code:

@ApiResponses(value = {
@ApiResponse(responseCode = "204", description = "Put the app under maitenance")})
@PutMapping
public ResponseEntity<Void> initInMaintenance(@NotNull @RequestBody String inMaintenance) {
        AvailabilityChangeEvent.publish(eventPublisher, this, Boolean.valueOf(inMaintenance) ? ReadinessState.REFUSING_TRAFFIC : ReadinessState.ACCEPTING_TRAFFIC);
        return ResponseEntity.noContent().build();
}

Filtre les appels et indiquer que l’application est en maintenance

Maintenant qu’on a codé les mécanismes de récupération du statut de l’application et de la mise en maintenance, on peut ajouter le mécanisme permettant de traiter ou non les appels entrants.
Pour ça on va utiliser un bon vieux filtre servlet.

Ce dernier aura la tâche de laisser passer les requêtes entrantes si l’application n’est pas en maintenance et de déclencher une MaintenanceException le cas échéant qui sera traité par la gestion d’erreur globale de l’application ( traité via un @RestControllerAdvice).

Pour que l’exception soit bien traitée par ce mécanisme, il faut le déclencher via le HandlerExceptionResolver.

@Component
public class CheckMaintenanceFilter implements Filter {
    private final static Logger LOGGER = LoggerFactory.getLogger(CheckMaintenanceFilter.class);
    @Autowired
    private ApplicationAvailability availability;

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver exceptionHandler;

    /**
     * Checks if the application is under maintenance. If it is and if the requested URI is not '/api/maintenance', it throws a <code>MaintenanceException</code>
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     * @throws info.touret.spring.maintenancemode.exception.MaintenanceException the application is under maintenance
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (availability.getReadinessState().equals(ReadinessState.REFUSING_TRAFFIC) &&
                !((HttpServletRequest) request).getRequestURI().equals(API_MAINTENANCE_URI)) {
            LOGGER.warn("Message handled during maintenance [{}]", ((HttpServletRequest) request).getRequestURI());
            exceptionHandler.resolveException((HttpServletRequest) request, (HttpServletResponse) response, null, new MaintenanceException("Service currently in maintenance"));
        } else {
            chain.doFilter(request, response);
        }
    }

}

Enfin, voici la gestion des erreurs de l’API:

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * Indicates that the application is on maintenance
     */
    @ResponseStatus(HttpStatus.I_AM_A_TEAPOT)
    @ExceptionHandler(MaintenanceException.class)
    public APIError maintenance() {
        return new APIError(HttpStatus.I_AM_A_TEAPOT.value(),"Service currently in maintenance");
    }

    /**
     * Any other exception
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler({RuntimeException.class, Exception.class})
    public APIError anyException() {
        return new APIError(HttpStatus.INTERNAL_SERVER_ERROR.value(),"An unexpected server error occured");
    }
}

Conclusion

On a pu voir comment intéragir simplement avec les APIS SPRING pour gérer le statut de l’application pour répondre à cette question :Est-elle disponible ou non?
Bien évidemment, selon le contexte, il conviendra d’ajouter un peu de sécurité pour que cette API ne soit pas disponible à tout le monde 🙂

Le code exposé ici est disponible sur Github. Le Readme est suffisamment détaillé pour que vous puissiez tester et réutiliser le code.

Utiliser GPG dans WSL2

Photo by Pixabay on Pexels.com

Pourquoi utiliser GPG ? Par exemple pour signer les commits GIT. Maintenant comment faire quand on est sous Windows 10 et qu’on souhaite utiliser le sous système Linux (WSL2)?

Sous GNU/Linux, l’installation et l’utilisation avec git est très simple. Avec WSL2,… il faut un peu d’huile de coude 🙂

Je vais tâcher de décrire dans cet article les différentes manipulations nécessaires pour:

  • Importer une clé GPG existante
  • Utiliser GPG pour signer mes commits dans WSL2

Importer une clé GPG existante

Export de la clé GPG

Identifier l’ ID de la clé

Lancez la commande suivante:

gpg --export ${ID} > public.key
gpg --export-secret-key ${ID} > private.key

Import

gpg --import public.key
gpg --import private.key

Vérification

Pour vérifier que la clé est bien configurée, vous pouvez lancer la commande suivante:

gpg --list-secret-keys --keyid-format LONG   alexandre@....
sec   rsa4096/CLE_ID 2019-12-20 [SC]
      ********************
uid                [  ultime ] Alexandre <alexandre@....>
ssb   rsa4096/SUB 2019-12-20 [E]

Si la clé n’est pas reconnue comme ultime ou comme de confiance, il faudra l’éditer:

gpg --edit-key CLE_ID
Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)
  1 = I don't know or won't say
  2 = I do NOT trust
  3 = I trust marginally
  4 = I trust fully
  5 = I trust ultimately
  m = back to the main menu
Your decision? 

Si vous ne voulez pas trop vous compliquer, je vous conseille de répondre 5.

Configuration GPG pour WSL2

Avant de configurer l’agent GPG, vous pouvez vous référer à cet article pour configurer GIT et GPG. La configuration est équivalente.

Ensuite, créez le fichier ~/.gnupg/gpg.conf avec le contenu suivant:

# Uncomment within config (or add this line)
# This tells gpg to use the gpg-agent
use-agent
# Set the default key
default-key CLE_ID

Puis créez le fichier ~/.gnupg/gpg-agent.conf avec le contenu ci-dessous:

default-cache-ttl 34560000
max-cache-ttl 34560000
pinentry-program /usr/bin/pinentry-curses

Le cache ici est défini en secondes. Il est mis ici à 400 jours.

Ce dernier fichier fait référence au programme pinentry. Vous pouvez vérifier sa présence grâce à la commande:

ls /usr/bin/pinentry-curses 

Si vous ne l’avez pas, vous pouvez l’installer grâce à la commande suivante:

sudo apt install pinentry-curses

Maintenant, on peut configurer l’environnement BASH en modifiant le fichier ~/.bashrc

# enable GPG signing
export GPG_TTY=$(tty)
if [ ! -f ~/.gnupg/S.gpg-agent ]; then
    eval $( gpg-agent --daemon --options ~/.gnupg/gpg-agent.conf )
fi
export GPG_AGENT_INFO=${HOME}/.gnupg/S.gpg-agent:0:1

Redémarrez ensuite WSL2 pour que ça soit pris en compte.

A la première utilisation de GPG ( par ex. lors d’un commit, vous aurez une interface Ncurses qui apparaîtra dans votre prompt WSL2. Vous aurez à renseigner le mot de passe de votre clé.

Installer Ubuntu 20.04 LTS sur un DELL Inspiron 13 5000

Les confinements se suivent et se ressemblent. Me voilà à installer Ubuntu sur un nouvel ordinateur.
A l’instar de l’ancien laptop que j’ai acheté pour mon aînée, j’ai acheté un DELL pour ma deuxième fille.
J’ai opté pour un DELL Inspiron 5301.


A l’instar de mon autre laptop, je j’ai pas pris de risques. J’ai opté pour un DELL qui est pleinement compatible avec Ubuntu. Oui j’aurai pu installer un ordinateur avec Ubuntu pré-installé, mais je n’ai pas eu le temps de faire un choix « serein ».


Configuration du BIOS

Voila les paramètres que j’ai appliqué:

Dans le menu "Storage" puis "SATA Operation": vous devez sélectionner AHCI au lieu de RAID.
Dans le menu "Change boot mode settings >UEFI Boot Mode" , vous devez désactiver le Secure Boot.

Une fois réalisé, vous pouvez redémarrer en appuyant sur la touche F12. Si vous n’arrivez pas à revenir sur le BIOS pour indiquer de booter sur votre clé USB, vous obtiendrez un écran d’erreur Windows dû à la configuration AHCI. Personnellement, en redémarrant une ou deux fois, j’ai obtenu un écran de démarrage avancé qui m’a permis de sélectionner le périphérique (ma clé USB) sur lequel démarrer.

Maintenant vous pouvez accéder à l’installeur Ubuntu et profiter.

Installation

J’ai eu plusieurs fois des popup « erreur rencontré ». Ce n’était pas bloquant. J’ai continué.

Tout s’est déroulé sans encombre. Le matériel est très bien reconnu.

Les seuls logiciels que j’ai installé sont pour l’instant : VLC, Minecraft ( obligatoire dans la famille ) et Chromium.

Gérer « efficacement » les fichiers de configuration dans les charts HELM

Dès qu’on veut déployer des environnements Kubernetes, Helm devient une des solutions à considérer.
Le déploiement des objets standards tels que deployment, autoscaler et autres se fait aisément car ces derniers ne changent pas d’un environnement à l’autre. Généralement on déploie la même infrastructure sur tous les environnements du développement à la production.

Bien évidemment on pourra limiter la taille des replicas sur l’environnement de développement par exemple mais au fond, le contenu des charts sera identique. Une des difficultés que l’on pourra rencontrer c’est dans la gestion des fichiers de configuration.

Je vais essayer d’exposer dans cet article comment j’ai réussi à gérer +/- efficacement (en tout cas pour moi) les fichiers de configuration dans les charts HELM.

Les config maps et secrets

Logiquement dans ce type d’architecture, les configmaps et secrets permettent le chargement des variables d’environnement et autres mots de passe. Cependant si vous utilisez certains frameworks qui nécessitent des fichiers de configuration, vous devrez charger les fichiers dans des volumes. Pour ces derniers, les volumes n’ont pas besoin d’être persistents.

Par exemple dans la configuration de votre deployment, vous pourrez configurer le montage d’un volume de la manière suivante:

  volumeMounts:
            - mountPath: /config
              name: configuration-volume
              readOnly: true
            - mountPath: /secrets
              name: secret-volume
              readOnly: true
      [...]
      volumes:
        - configMap:
            defaultMode: 420
            name: configuration
          name: configuration-volume
        - name: secret-volume
          secret:
            defaultMode: 420
            secretName: secrets

Pour intégrer un fichier binaire, on pourra le faire de la manière suivante dans le template HELM:

apiVersion: v1
# Definition of secrets
kind: Secret
[...]
type: Opaque
# Inclusion of binary configuration files
data:
  my_keystore.jks: {{ .Files.Get "secrets/my_keystore.jks" | b64enc }}

Vous pouvez définir les fichiers directement dans vos configmaps. Cependant, si vos fichiers sont volumineux, vous aurez du mal à les maintenir. Personnellement, j’opte pour mettre les fichiers de configuration à coté et les charger dans le configmap.

On pourra procéder de la manière suivante:

apiVersion: v1
kind: ConfigMap
[...]

data:
  my.conf:   {{- (.Files.Glob "conf/*").AsConfig | nindent 2 }} 

Livrables agnostiques

Une bonne pratique de développement logiciel est d’externaliser la configuration de vos environnements (ex. l’URL JDBC de la base de données) des livrables. Les charts HELM n’échappent à la règle.

On peut stocker la configuration de chaque environnement dans le chart, mais dans ce cas, on perdra beaucoup de souplesse lors des mises à jour des propriétés et cela nous imposera une nouvelle version.

On a plusieurs niveaux d’externalisation. Le premier est dans le chart. Vous pouvez externaliser les différentes valeurs dans le fichier values.yml. Ci dessous un exemple avec un autoscaler:

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  labels:
  [...]
spec:
  maxReplicas: {{ .Values.myapp.maxReplicaCount }}
  minReplicas: {{ .Values.myapp.minReplicaCount }}
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
   [...]
  targetCPUUtilizationPercentage: {{ .Values.myapp.replicationThreesold }}

Les valeurs sont décrites comme suit:

myapp:
  minReplicaCount: "2"
  maxReplicaCount: "6"
  replicationThreesold: 80

Pour externaliser les valeurs d’environnement, vous pourrez donc externaliser un autre fichier values.yml qui sera appliqué au déploiement. Les valeurs de ce dernier surchargeront les valeurs définies dans le chart.
Il est important de noter également que les données présentes dans les fichiers de configuration (ex. fichier application.properties) peuvent être « variabilisées » et surchargées par le même mécanisme. Vous aurez à utiliser la commande tpl.

apiVersion: v1
kind: ConfigMap
metadata:
  name: configuration
  labels:
    [...]
data:
  application.properties: 
    |- 
    {{ tpl (.Files.Get "conf/application.properties") . | nindent 4}} 

Conclusion

Vous l’aurez compris, les charts HELM n’échappent pas aux règles déjà connues de gestion des environnements et des livrables. Même si il y a quelques subtilités à connaître pour intégrer des fichiers de configuration par exemple, les grands principes restent les mêmes.

K8S, HELM et Cie: au delà de la hype

Depuis quelques années, Kubernetes (K8S) et son écosystème deviennent l’environnement d’ exécution à la mode. Certaines personnes veulent déployer sur cet environnement en mettant en avant ses capacités de scalabilité. D’autres font du bashing (souvent) justifié sur la complexité et le coût de mise en œuvre d’une telle plateforme.
Vous l’aurez compris, cette technologie n’échappe pas au cycle du hype et à la fameuse courbe du Gartner.

Après quelques expériences sur cette plateforme ( et beaucoup sur d’autres 😀 ) je vais essayer de peser le pour et le contre qui m’apparaissent importants.
Bien évidemment, ce n’est que mon avis, j’ai sans doute omis certaines informations qui pourraient être indispensables pour d’ autres.

Pourquoi et dans quelles conditions il ne faut pas utiliser K8S ?

Avant de présenter les avantages des applications cloud, je vais essayer de réaliser l’anti thèse de mon propos.

En avez vous (vraiment) besoin ?

Vaste sujet et question délicate pour la population informaticienne qui a tendance à suivre les tendances du marché.

Avant de foncer tête baissée dans cette technologie qui est très intéressante au demeurant, il est important de se poser ces quelques questions:

  • Est-ce que mes SLO sont contraignantes?
  • Quel le cycle de déploiement de mes applications?
  • Qui gère les environnements ?

Bref, il faut savoir si le jeu en vaut la chandelle. Si vous avez une application qui doit scaler dynamiquement, encaisser les pics, et avoir du zero downtime durant les mises à jour, Kubernetes est fait pour vous. Si vous avez une application de gestion qui n’a pas d’exigences fortes si ce n’est de répondre aux besoins fonctionnels, l’utilisation de Kubernetes est discutable.

Êtes vous taillé pour ?

Kubernetes et son écosystème peuvent s’avérer complexes à appréhender. Si votre entreprise opte pour une utilisation « on premise« , c’est pire. Vous devrez avoir une équipe dédiée qui gérera cette plateforme et offrir une expertise aux équipes de développement.
Ne vous trompez pas. Si votre rôle est de développer des applications métier, il vous sera très difficile d’avoir également une expertise sur l’administration de cette plateforme. Vous pourrez l’utiliser et être à l’aise, mais l’administration d’une telle technologie est très compliquée.

Le seul conseil que je pourrais vous donner, c’est de ne partir sur Kubernetes que si vous avez une équipe support à disposition. C’est vrai si vous utilisez des services du Cloud tels que Google Cloud ou AWS. Ça l’est encore plus si vous utilisez des services « on premise » tels qu’ Openshift.

Est-ce que vos développements sont « cloud native » ?

Au delà de la plateforme, vous devrez monter en compétence sur le développement et la conception de vos applications.

Il vous faudra prendre en considération les 12 facteurs clés dans vos applications. Il n’est pas forcément la peine de passer sur des microservices. Il est également possible de faire des monolithes modulaires qui peuvent être légers et stateless. Beaucoup de ces facteurs sont communément admis comme des bonnes pratiques de développement logiciel (ex. Il faut une intégration continue).

Aussi, cela va sans dire, il faut également monter (réellement) en compétence sur les conteneurs et leurs contraintes. Si vous n’avez pas l’habitude de travailler avec des conteneurs ( construction, déploiement, disponibilité d’une registry). Il est préférable de définir une trajectoire avec des étapes intermédiaires.

Bref, tous ces sujets doivent être adressés et compris pour toutes les parties prenantes de vos équipes que ça soit les développeurs, les chefs de projet et les équipes métiers à une moindre mesure. Cette technologie représente réellement un grand pas à franchir. Si vous ne vous sentez pas de le faire, ou si vous devez gagner en maturité sur ces sujets, attendez avant de vous lancer sur Kubernetes.

On ne pourra jamais vous reprocher de ne pas opter sur Kubernetes si vous ne remplissez pas tous les pré-requis. Pour ce qui est du contraire…

Avez vous des interactions avec des services tiers qui sont compatible avec Kubernetes ?

Quand vous restez dans votre cluster Kubernetes, généralement, tout va bien. Dès que vous avez des interactions avec des services tiers, ça peut se compliquer.
En effet, généralement vous devrez vous connecter à des services tiers qui ne sont pas orienté cloud : des boitiers crypto, des passerelles de transfert, …
Il se peut que certains protocoles soient également incompatibles avec Kubernetes. Il vous faudra vous assurer que tout la galaxie de logiciels et systèmes gravitant autour de votre application sera compatible avec une telle architecture. Ceci n’est pas une mince affaire. L’aide d’une équipe support (voir ci-dessus) vous sera d’une grande utilité.

Pourquoi sauter le pas ?

La scalabilité et la résistance à la panne

Personnellement, la première fonctionnalité qui m’a intéressé c’est la gestion de la scalabilité. Si vous avez des objectifs de 99.9% de disponibilité. Kubernetes sera une plus value indéniable dans votre architecture. Après quelques jours heures à batailler avec les fichiers YAML, vous pourrez gérer automatiquement la scalabilité en fonction de plusieurs indicateurs qu’ils soient techniques (ce sont les plus faciles à gérer) ou un peu plus métier en utilisant Prometheus – et oui encore une technologie supplémentaire à connaître.

En effet, au lieu de vous en soucier une fois arrivé en production, vous aurez lors du développement l’obligation de prendre en considération l’observabilité de votre application. Par exemple, vous aurez à renseigner si votre application est prête et/ou disponible pour traiter les requêtes. Ces indicateurs vous permettront de scaler automatiquement et de re-créer si nécessaire un POD en cas de panne.

J’ai trouvé que cette pratique était vertueuse. Bien évidemment, pas besoin d’être sur Kubernetes pour avoir de l’observabilité dans des applications. Par contre, ici, c’est obligatoire et implémenté dès le développement.

La scalabilité automatique est aussi très intéressante. On a souvent vu des serveurs en production qui n’étaient pas suffisamment utilisés. Ici vous n’aurez que les instances nécessaires pour votre cas d’utilisation.
La contrainte que l’on peut voir à cette fonctionnalité et qu’on ne maitrise pas complètement le nombre d’instances disponibles. C’est Kubernetes qui s’en charge en prenant en compte le paramétrage que vous aurez renseigné dans vos templates HELM.

Le déploiement

Avant de déployer (dans la vraie vie), vous aurez à mettre en place un pipeline CI/CD qui orchestre les différents déploiements sur tous vos environnements. Attention, ce n’est pas une mince affaire 🙂 !

Une fois réalisé, vous verrez automatiquement le gain. Vos déploiements seront réellement fluides. Bon OK, on peut le faire sur des VMS standards. Mais on peut améliorer la procédure de déploiement pour mettre en place du zero downtime pour ne pas interrompre le service lors d’un déploiement.

strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0

L’Infrastructure As Code

Quand on pense à Kubernetes, et au cloud, on ne pense pas trop à l’Infrastructure As Code au début. Cependant, cette pratique est pour moi l’une des plus utiles.

En effet, avoir votre système décrit dans des fichiers, versionnés vous permet de le tester dès le développement. Ça évite ( dans la majorité des cas ) les erreurs lors des installations d’environnement. La mise à jour des logiciels est largement accélérée.

Bien évidemment, il existe Terraform et Ansible pour le provisionning des environnements. Ici je trouve qu’on pousse le concept encore plus loin. L’automatisation est à mon avis poussé à paroxysme.
Prenons par exemple la gestion des systèmes d’exploitation. La mise à jour sur des serveurs physiques ou virtuels peut prendre énormément de temps et générer des erreurs. Avec de l’infra as code, ceci est testé et validé automatiquement via des tests unitaires dès l’environnement de développement.
On peut suivre la gestion des environnements via un gestionnaire de sources et la promotion vers les autres environnements (recette[1-n], pré-production, production) est grandement accélérée.

Conclusion

Bon, vous l’aurez peut être compris, cette galaxie de technologies est intéressante et peut vous aider dans vos projets. Avant d’arriver à l’utiliser sereinement, il vous faudra sans doute définir une trajectoire et appréhender plusieurs sujets avant d’arriver à déployer vos applications sur un cloud interne ou externe.
J’espère que cet article vous aura permis de mettre en évidence les pour et contre d’une telle technologie et le cas échéant vous donnera envie de franchir le pas.

Music Scores As Code

Derrière ce nom pompeux qui peut effrayer, je vais essayer d’expliquer dans cet article comment on peut versionner facilement ses partitions et les publier sur le web.

En cherchant comment mettre de la documentation technique avec des diagrammes PlantUml dans des repos GITLAB et générés avec des pipelines, je me suis mis dans la tête de faire la même chose avec des partitions 🙂

Depuis plusieurs années, j’utilise lilypond pour créer mes partitions. C’est un peu difficile de s’y mettre, mais une fois la syntaxe assimilée, la saisie d’une partition est beaucoup plus efficace. Le rendu des partitions est vraiment optimisé.
Si vous voulez plus de détails sur le pourquoi du comment je vous conseille cette page.

Vous trouverez des exemples sur le site.

J’ai donc eu l’idée de:

  • Stocker ces partitions sur un repo github (jusque là rien d’exceptionnel)
  • Générer automatiquement les partitions au format PDF, PNG et MIDI via une github action (ça commence à devenir intéressant…)
  • Les publier avec les github pages (tant qu’à faire 🙂)

Stockage

Pourquoi stocker dans un référentiel de sources tel que Github ? Pour les non informaticiens : les partitions sont stockées au format texte.

\version "2.12.1"

\header {
  title="Try a little tenderness"
  composer="Harry Woods, Jimmy Campbell & Reg Connely"
  subtitle = "Commitments Version"
  %poet = "Poete"
  instrument = "Piano"
  editor = "L'éditeur"
  %meter=\markup {\bold {"Remarque sur le rhythme"}}
  style = "Soul"
  maintainer = "Alexandre Touret"
  maintainerEmail = "alexandre.touret@free.fr"
  maintainerWeb = "http://blog.touret.info"     
  lastupdated = ""
  source = "Music room"
  footer = "Footer"
  copyright =\markup {\fontsize #-1.5
 "Delivered by A TOURET"}
}
upper=
\relative c'{
  \clef treble
  \time 4/4
  \tempo 4=176
  \key g \major
  
  d'2 (b4 e
  d2 b4 a 
  g2 g2 
  <e g,>2) <fis, c' d> 
  \bar "||"

GIT et GITHUB permettent de versionner facilement et pouvoir faire facilement un retour arrière en cas d’erreur.
Aussi, GITHUB offre des fonctionnalités « sociales » et collaboratives qui facilitent la revue des modifications ( en cas de travail à plusieurs ).
Bref, ça offre la sécurité d’une sauvegarde et la possibilité d’un retour arrière en cas d’erreur.

Générer les partitions avec une github action

Les github actions sont des outils permettant:

GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.

J’ai donc décidé de créer un workflow qui permet de générer les partitions au format lilypond.

J’ai mis à disposition le code sur github sous licence GNU GPLv3. Elle est utilisable telle quelle.

Pour créer l’action, il faut créer un fichier action.yml à la racine du repo. Voici le contenu

name: 'Lilypond Generator'
description: 'Run Lilypond tool with the given set of arguments to generate music sheets'
author: '@alexandre-touret'

inputs:
  args:
    description: 'Arguments for Lilyponid'
    required: true
    default: '-h'

runs:
    using: 'docker'
    image: 'Dockerfile'
    args:
      - ${{ inputs.args }}

branding:
  icon: 'underline'
  color: 'blue'

Vous aurez compris que ce fichier fait référence à une image Docker. Cette dernière n’est ni plus ni moins qu’une Debian avec lilypond d’installé.

Pour l’utiliser dans un repo github, on peut créer une action qui l’utilise. Voici un exemple:

jobs:
  build_sheets:
    runs-on: ubuntu-latest
    env:
        LILYPOND_FILES: "*.ly"
    steps:
      - name: Checkout Source 
        uses: actions/checkout@v1
      - name: Get changed files
        id: getfile
        run: |
          echo "::set-output name=files::$(find ${{github.workspace}} -name "${{ env.LILYPOND_FILES }}" -printf "%P\n" | xargs)"
      - name: LILYPOND files considered echo output
        run: |
          echo ${{ steps.getfile.outputs.files }}
      - name: Generate PDF music sheets
        uses: alexandre-touret/lilypond-github-action@master
        with:
            args: -V -f --pdf ${{ steps.getfile.outputs.files }}

A la dernière ligne on peut passer les arguments nécessaires à lilypond.

Publication

La c’est l’étape la plus facile :). Il suffit d’activer les github pages et de commiter et pusher les partitions générées

 - name: Push Local Changes
        run: |
          git config --local user.email "${{ secrets.GIT_USERNAME }}"
          git config --local user.name "${{ secrets.GIT_EMAIL }}"
          git add .
          git commit -m "Add changes" -a
      - name: Push changes
        uses: ad-m/github-push-action@master
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

Il suffit de créer une page index.md à la racine et d’ajouter des liens vers les partitions générées ( dans mon cas, ça se passe dans le répertoire /docs ).
Vous pouvez trouver un exemple ici.

Conclusion

Voila comment on peut générer un site avec des partitions crées avec Lilypond.
Vous trouverez les différents liens ci-dessous. Peut être que je publierai cette action sur le marketplace une fois que j’aurai publié une documentation digne de ce nom :).

Utiliser des GITHUB Actions pour déployer dans Google Kubernetes Engine

A mes heures perdues, je travaille sur un « POC/side project qui n’aboutira pas et je m’en fiche » basé sur Quarkus. J’ ai choisi d’utiliser les langages et composants suivants :

Oui, tant qu’à faire, autant aller dans la hype …

Mon projet est sur GITHUB. Pour automatiser certaines actions et, disons-le, par fierté personnelle, j’ai choisi d’automatiser certaines actions par la mise en œuvre de pipelines CI/CD.
Depuis peu, GITHUB a intégré un mécanisme de pipeline : GITHUB Actions.

Ça permet, entre autres, de lancer des processus automatisé sur un push ou sur une action pour un commit GIT.

La force de l’outil est, selon moi, de facilement s’intégrer avec beaucoup de services du cloud ( sonarcloud, google cloud, heroku,…). On aime ou on n’aime pas, mais chez Microsoft, l’intégration ils savent faire.

Par exemple, si on veut lancer une compilation lors d’un push, on peut placer un fichier .github/workflows/build.xml avec le contenu :

name: CI

on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11
      - name: Build with Gradle without testing
        run: ./gradlew build -x test

Coté GITHUB, vous verrez l’exécution sur un écran dédié

Vous pouvez créer autant de workflows que vous souhaitez (si votre projet est en libre accès).
Pour chaque workflow, on peut définir et utiliser des jobs. Les logs d’exécution sont disponibles dans ce même écran:

Worflows implémentés

J’ai choisi d’implémenter les workflows suivants:

  • CI: Build sur la feature branch
  • CD: Build sur master branch et déploiement

On obtient donc dans mon cas:

Ce n’est pas parfait. Loin de là. Dans la « vraie vie », pour une équipe de dev, je l’améliorerai sans doute par un build docker dans les features branches, une validation formelle et bloquante de l’analyse sonar, etc.
Pour un dev perso ça suffit largement. Le contenu de la branche master est compilé et une image docker est crée pour être déployée automatiquement dans GKE.

Analyse SONAR

J’ai choisi d’utiliser sonarcloud pour analyser mon code. C’est gratuit pour les projets opensource. L’analyse se fait simplement:

  sonarCloudTrigger:
    name: SonarCloud Trigger
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11
      - name: SonarCloud Scan
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: ./gradlew jacocoTestReport sonarqube

Dans ce job j’utilise deux secrets. Ce sont des tokens qui permettent de ne pas stocker en dur les données dans les repos GITHUB.

Création d’une image Docker et déploiement dans le registry GITHUB

Ici aussi, ça se fait simplement. La preuve :

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 11
      - name: Build in JVM Mode with Gradle without testing
        run: ./gradlew quarkusBuild  [1]
      - name: Branch name
        run: echo running on branch ${GITHUB_REF##*/}
      - name: Build the Docker image Quarkus JVM
        run: docker build -f src/main/docker/Dockerfile.jvm -t docker.pkg.github.com/${GITHUB_REPOSITORY}/music-quote-jvm:latest .  [2]
      - name: Login against github docker repository
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: docker login -u ${GITHUB_ACTOR} -p ${GITHUB_TOKEN}  docker.pkg.github.com   [3]
      - name: Publish the Docker image Quarkus JVM
        run: docker push docker.pkg.github.com/${GITHUB_REPOSITORY}/music-quote-jvm:latest  [4]
  1. Création du binaire
  2. Création de l’image docker en utilisant la commande docker et le Dockerfile fourni par Quarkus
  3. Identification sur la registry Docker de GITHUB
  4. Déploiement de l’image

Pour plus de détails sur la variable GITHUB_TOKEN, vous pouvez lire cet article de la documentation.

Déploiement dans Google Kubernetes Engine

Mon application est pour l’instant architecturée comme suit (attention c’est compliqué):

Pour la déployer dans Google Kubernetes Engine, j’ai besoin d’ implémenter cette « architecture » par les objets Kubernetes suivants:

J’utilise les objets suivants:

  • Des services pour exposer la base de données ainsi que l’application
  • Un deployment pour l’application
  • Des pods car à un moment, il en faut…
  • Un statefulset pour la base de données

Vous pourrez trouver la définition de tous ces objets au format yaml via ce lien. J’ai fait très simple. Logiquement j’aurai du créer un volume pour les bases de données ou utiliser une base de données en mode PAAS.

Pour lancer le déploiement, il faut au préalable créer un secret ( fait manuellement pour ne pas stocker d’objet yaml dans le repository GITHUB) pour se connecter au repo GITHUB via la commande suivante:

kubectl create secret docker-registry github-registry --docker-server=docker.pkg.github.com --docker-username=USER--docker-password=PASSWORD --docker-email=EMAIL

On peut faire pareil pour les connexions base de données. J’ai mis dans un configmap pour ne pas trop me prendre la tête…

Après le déploiement via le pipeline se fait assez simplement:

      [...]
      - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          version: '286.0.0'
          service_account_email: ${{ secrets.GKE_SA_EMAIL }}
          service_account_key: ${{ secrets.GKE_SA_KEY }}
          project_id: ${{ secrets.GKE_PROJECT }}
      # Get the GKE credentials so we can deploy to the cluster
      - run: |-
          gcloud container clusters get-credentials "${{ secrets.GKE_CLUSTER }}" --zone "${{ secrets.GKE_ZONE }}"
      # Deploy the Docker image to the GKE cluster
      - name: Deploy
        run: |-
          kubectl apply -f ./k8s     

J’utilise les « actions » fournies par Google.

Conclusion

Pour que ça marche il y a pas mal d’étapes préalables ( des tokens à générer, un utilisateur technique, …).
J’ai essayé de les référencer dans le README du projet.
Si vous voulez tester l’intégration Kubernetes dans le cloud google, sachez que vous pouvez disposer d’un crédit de 300€ valable un an. Attention, avec ce genre d’architecture, ça part vite…

Améliorer le temps de démarrage de Debian 10

Mon PC Lenovo a un SSD. Le temps de démarrage est actuellement de 11 sec. Ça commence à faire pas mal… J’ai eu donc envie de me pencher sur l’optimisation du démarrage ( encore une fois) . Voici comment gagner (facilement) quelques secondes au démarrage.

Tout d’abord, vous devez analyser les services qui prennent du temps au démarrage. Vous pouvez le faire avec cette commande:

systemd-analyze plot > plot.svg

J’ai obtenu le graphique suivant:

Configuration GRUB

La première manipulation à réaliser est de désactiver le timeout de GRUB. Pour celà, vous pouvez modifier la variable GRUB_TIMEOUT dans le fichier /etc/default/grub:

GRUB_TIMEOUT=0

Ensuite, vous devez mettre à jour la configuration GRUB en exécutant cette commande:

sudo update-grub2

Au prochain reboot, vous ne verrez plus le menu GRUB.

Configuration NetworkManager

Dans mon cas, le service NetworkManager-wait-online.service prenait près de 9 secondes. Après avoir lu plusieurs billets et rapports de bug, je me suis aperçu que je pouvais le désactiver au boot. Vous pouvez le faire en lançant la commande suivante

sudo systemctl disable NetworkManager-wait-online.service

Configuration Apt

Un autre service qui prenait pas mal de temps était apt-daily.timer qui vérifiait au boot qu’il y avait des mises à jour de l’OS. Après quelques recherches, j’ ai vu qu’on pouvait soit le désactiver ( ce qui n’est pas recommandé pour les mises à jour de sécurité ) soit décaler la recherche. J’ai choisi cette solution. Vous devez donc exécuter la commande suivante:

sudo systemctl edit apt-daily.timer

Et renseigner le contenu suivant:

[Timer]
OnBootSec=15min
OnUnitActiveSec=1d
AccuracySec=1h
RandomizedDelaySec=30min

Ce service sera donc lancé 15 minutes après le boot. Ce qui est largement suffisant.

[EDIT] Vous pouvez appliquer la même configuration pour le service apt-daily-upgrade en exécutant la commande:

sudo systemctl edit apt-daily-upgrade.timer

Ensuite, vous pouvez recharger la configuration en exécutant cette commande:

sudo systemctl daemon-reload

Résultats

Après ces quelques manipulations qui peuvent prendre 5 minutes grand maximum, j’ai réussi à optimiser le boot en réduisant le démarrage à 5 secondes!

Vous pourrez trouver le détail ci-dessous:

Répéter avec JjazzLab tout seul dans son garage

Avec les contraintes liées au confinement, les répétitions se font de plus en plus rares. Pour ne pas perdre la main, il y a quelques logiciels qui permettent de jouer d’un instrument et d’ improviser tout en ayant une bande son en fond musical.

Il y a plusieurs logiciels payants/propriétaires sur différentes plateformes:

J’ai découvert ce dernier récemment en naviguant sur le site Linux Mao. Il a l’avantage d’être gratuit (le moteur est sous licence LGPL 3.0, pas le logiciel en tant que tel), de fonctionner sous GNU/LINUX, d’ offrir un son pas mal du tout et de permettre la configuration de la dynamique au fur et à mesure du morceau.

Je vais expliquer comment l’installer sur Debian.

Configuration Midi

Activation des périphériques virtuels MIDI

Créer un fichier /etc/modules.load.d/midi.conf avec le contenu suivant:

snd-virmidi

Ensuite créer le fichier /etc/modprobe.d/midi.conf avec le contenu suivant:

options snd-virmidi midi_devs=1

Logiquement à ce stade, lors du prochain reboot, vous aurez un périphérique virtuel MIDI activé. En attendant vous pouvez lancer la commande suivante

$ sudo modprobe snd-virmidi midi_devs=1

Synthétiser du MIDI

Pour faire fonctionner ce logiciel, il faut installer une banque de son ( au format SF2) et un logiciel permettant de l’utiliser pour synthétiser du MIDI.
La banque de son recommandée est disponible via ce lien. Téléchargez là et copiez la dans un répertoire accessible.

Pour le second, il vous faudra installer fluidsynth. Voic les quelques commandes à lancer:

$ sudo apt install fluid-synth qsynth

Petite vérification…

Avant d’ aller plus loin dans la configuration de fluidsynth, vous pouvez vous assurer que tout est OK en récupérant un fichier MIDI et en lançant la commande suivante:

$ fluidsynth -a pulseaudio -m alsa_seq -l -i /opt/JJazzLab-2.0-Linux/JJazzLab-SoundFont.sf2  MIDI_sample.mid

Normalement vous devriez avoir du son.

Configurer fluidsynth

Lancez qsynth et cliquez sur le bouton « configuration »

Vous trouverez ci-dessous la configuration que j’ai appliqué. Elle diffère légèrement de celle présentée dans la documentation.

Pensez à redémarrer fluidsynth après application de ces nouveaux paramètres.

Configurer aconnect

Disclaimer: La c’est la partie la plus obscure…

Il faut maintenant « brancher » la sortie du synthétiseur virtuel MIDI à fluidsynth pour que le son MIDI soit interprété par ce dernier à travers sa banque de son. Ce n’est pas intuitif, je vous avais prévenu …
Je ne vous parle pas de la pseudo interface graphique à aconnect. La ligne de console est plus parlante ( c’est pour dire ) .

Exécutez la commande suivante:

$ aconnect -lo                                            
client 14: 'Midi Through' [type=noyau]
    0 'Midi Through Port-0'
client 24: 'Virtual Raw MIDI 2-0' [type=noyau,card=2]
    0 'VirMIDI 2-0     '
client 128: 'FLUID Synth (JJLAB)' [type=utilisateur,pid=17838]
    0 'Synth input port (JJLAB:0)'

Dans mon cas, je vais avoir à connecter le client 24:0 au synthétiseur 128:0 grâce à la commande :

$ aconnect 24:0 128:0

Maintenant, si on relance la commande aconnect -lo on obtient le résultat suivant:

client 14: 'Midi Through' [type=noyau]
    0 'Midi Through Port-0'
client 24: 'Virtual Raw MIDI 2-0' [type=noyau,card=2]
    0 'VirMIDI 2-0     '
	Connexion À: 128:0
client 128: 'FLUID Synth (JJLAB)' [type=utilisateur,pid=17838]
    0 'Synth input port (JJLAB:0)'
	Connecté Depuis: 24:0

Attention, cette commande devra être lancée ( ainsi que fluidsynth) avant chaque démarrage de jjazzlab.

Installation de Jjazzlab

Téléchargez les binaires sur ce site, puis décompressez l’archive dans le répertoire /opt par ex.

Vous devez également installer java

$ sudo apt install openjdk-11-jdk

Ensuite, vous devez créer le fichier ~/.local/share/applications/jjazzlab.desktop avec le contenu suivant:

[Desktop Entry]
Type=Application
Name=JJazzLab
GenericName=JJazzLab
Icon=
Exec="/opt/JJazzLab-2.0-Linux/bin/jjazzlab"
Terminal=false
Categories=Audio;Music;Player;AudioVideo;

Maintenant vous pouvez directement démarrer JJazzlab via le menu.

Configuration

Une fois jjazzlab démarré, vous devez aller dans le menu « Tools>Options » et sélectionnez les valeurs suivantes:

Ouvrez un fichier example (ex. sunny )

Cliquez sur le menu décrit par un clavier

Puis configurez comme suit:

Maintenant vous pouvez télécharger les standards fournis sur le site et improviser dessus 🙂

Erreur 139 à l’exécution d’un container docker

Voici un rapide article sur un problème rencontré récemment. Lors de l’exécution d’un container docker, j’ai eu une erreur SIGSEGV 139. Un crash avec aucune log.

Bref que du bonheur 🙂



Avant d’aller plus loin voici mon environnement:

Après quelques recherches, je me suis rendu compte qu’on pouvait reproduire ce comportement en exécutant cette commande:

docker run -it gcc:4.8.5

Une des raisons trouvées serait un problème de compatibilité avec le noyau 4.8.5 (oui ça remonte…).
Une solution est d’activer l’émulation vsyscall.


Voici la configuration à effectuer:
Dans le fichier /etc/default/grub, ajouter la ligne suivante:

GRUB_CMDLINE_LINUX_DEFAULT="quiet vsyscall=emulate"

Puis lancer les commandes suivantes:

$ sudo update-grub 
$ sudo reboot

Maintenant le container devrait pouvoir s’exécuter correctement.