Comment Monday utilise Go et comment fonctionne-t'il ?

Monday - Dev tool for microservices

De plus en plus d'entreprises et donc de développeurs sont amenés à travailler sur des applications micro-services. C'est super parce-que vous pouvez développer des applications plus maintenables dans le temps et dont vous avez la maîtrise du périmètre métier. Cela vous permet aussi peut-être d'approcher les technologies de containeurs et d'orchestration de containeurs comme Kubernetes. Vous avez donc des environnements vous permettant de déployer tous ces services sur Kubernetes.

Cependant, lorsqu'il s'agit de se placer du côté du développeur, chacun a souvent sa façon de faire : ses astuces pour lancer le micro-service sur lequel il travaille sur sa machine et le tester, peut-être en faisant du port-forward sur un environnement pour éviter d'avoir à tout lancer en local. C'est un cas que j'ai rencontré récemment lors d'une mission et il me semblait intéressant de répondre à ces problématiques :

  • Unifier l'environnement des développeurs travaillant sur des micro-services pour avoir de meilleures interactions entre eux (et faciliter l'arrivée des nouveaux venus),
  • Unifier les configurations des micro-services pour éviter aux développeurs de les changer, chacun en fonction de leurs besoins,
  • Permettre de choisir "à la carte" les applications que nous souhaitons exécuter en local,
  • Permettre aussi éventuellement de ne pas exécuter certaines applications en local mais de contacter d'autres applications sur un environnement directement (via du port-forward),
  • Enfin, éviter au développeur de lancer 10 terminals pour lancer 10 micro-services et autant de lignes de commande.

Ainsi, chacun aurait la souplesse de travailler de la façon dont il le souhaite et ce de façon simple. Pour cela, j'ai créé http://github.com/eko/monday, un outil en ligne de commande, développé avec le langage Go, qui permet une utilisation simple, un partage de configuration entre les développeurs et une souplesse sur la configuration des projets.

Pour une petite preview de l'outil, qui s'exécute simplement en tapant "monday" dans son terminal et en choisissant un projet, rendez-vous ici :

https://asciinema.org/a/LyhH2Gdz4JBo5bThPisZPTLpe

Comment fonctionne Monday ?

La configuration est déclarée dans un (ou plusieurs) fichier(s) YAML et il est donc possible de la ppartager avec chaque développeur de l'équipe. Je ne détaillerais pas la configuration dans cet article, pour plus de détail, je vous invite à vous rendre sur la [https://github.com/eko/monday/wiki/Configuration](page Wiki dédiée sur le repository GitHub du projet).

Voici un schéma expliquant les principes de Monday et nous allons détailler son fonctionnement au long de cet article :

Monday - Dev tool for microservices

L'outil se compose en plusieurs composants, ici représentés en violet :

  • Runner : Il s'agit du composant permettant l'exécution des applications ou services tiers en local (sur votre machine),
  • Watcher : Ce composant permet de re-charger une application lorsqu'un changement est effectué dans la base de code : pratique lorsque vous travaillez sur des micro-services en Go par exemple,
  • Forwarder : Lorsque vous ne souhaitez pas lancer une application en local mais que vous préférez utiliser l'application sur un environnement directement, ce composant vous permet de faire du forward (local ou remote) vers Kubernetes, en SSH ou simplement en mode proxy TCP,
  • Proxy : Ce composant permet, à la fois pour les forwards ou les applications locales de mapper un hostname avec une IP privée afin de vous permettre d'unifier les configurations de vos applications.

Nous allons maintenant rentrer un peu plus dans le détail de chacun de ces composants dans la suite de cet article pour comprendre leur fonctionnement technique.

Runner : Comment les processus sont lancés en local ?

L'idée de ce composant Runner est donc d'exécuter plusieurs applications sur votre machine, de vous afficher les logs de ceux-ci sur une sortie unique et bien sûr de ne pas oublier de les arrêter lorsque décidez de mettre fin à l'application Monday.

Les processus sont exécutés via le package Go https://golang.org/pkg/os/exec/ :

cmd := exec.Command(application.Executable, application.Args...)
// ...
err := cmd.Run()

Lorsque le composant Watcher détecte un changement, il faut donc couper ce processus et en lancer un nouveau. En fonction des processus lancés, il se peut que ceux-ci se ne soient pas tués par le composant Runner car celui-ci terminerait uniquement le processus enfant. En effet, si vous prenez un exemple simple d'un script.sh qui lancerait une commande, voici ce que nous obtenons en terme de processus :

  PID  PPID  PGID STAT COMMAND
56503 55708 56503 S+   /bin/bash ./script.sh
56504 56503 56503 S+   sleep 20

Ce qu'il est important de noter ici, c'est la notion de "PGID" (pour Process Group ID), qui regroupe notre processus parent et ses enfants dans un même groupe. En Go, pour que le PGID soit affecté, il nous faut ajouter à notre commande la ligne suivante :

cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

Ainsi, lorsque nous souhaiterons couper notre processus (regroupant parent et enfants), nous aurons simplement à tuer le process group id, de cette façon :

pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err == nil {
    syscall.Kill(-pgid, 15)
}

Tous nos processus sont maintenant bien terminés et peuvent être relancés sereinement !

Proxy : Comment fonctionne-t'il ?

Le proxy est la brique centrale de Monday, il permet l'attribution d'un hostname à une nouvelle IP privée et de mapper des ports sur cette IP. Go ne propose malheureusement rien permettant de manipuler les interfaces réseau (un package https://golang.org/src/net/interface.go permet uniquement d'afficher des informations), nous utilisons donc encore une fois les commandes et ifconfig pour attribuer une IP à l'interface réseau.

Nous utilisons en effet l'interface loopback lo0 et y attribuons une IP pour chaque application :

command := "ifconfig"
args := []string{"lo0", "alias", ip.String(), "up"}

err := exec.Command(command, args...).Run()
// ...

Afin de tester si un port est déjà utilisé pour une IP et un port donné, rien de plus simple, il suffit d'essayer d'ouvrir une connexion sur cette IP et le port associé :

conn, err := net.Dial("tcp", fmt.Sprintf("%s:%s", ip.String(), port))
if err == nil {
    // No error returned, IP/port are available
    return net.IPv4(a, b, c, byte(i)), nil
}
conn.Close()
// Here, IP/Port is not available, search for another

Une fois les IPs et hostnames attribués, dans le cas ou ce hostname (par exemple : grpc-api.svc.local) est destiné à pointer sur un service tiers non lancé en local, nous lançons un proxy TCP permettant de lire et écrire les réponses du client distant en local, de cette façon :

listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", localIP, localPort))
if err != nil {
    fmt.Printf("❌  Could not create proxy listener for '%s:%s' (%s): %v\n", localIP, localPort, hostname, err)
}

for {
    client, _ := listener.Accept()
    target, _ := net.Dial("tcp", fmt.Sprintf("%s:%s", remoteHostname, proxyPort))

    // Write response to local client
    go func() {
        defer client.Close()
        defer target.Close()
        io.Copy(client, target)
    }()

    // Send request to remote client
    go func() {
        defer client.Close()
        defer target.Close()
        io.Copy(target, client)
    }()
}

Ainsi, nous avons un système nous permettant d'avoir un réel proxy TCP à double sens.

Forwarder : Comment fonctionne le proxy "local" ?

Ce que j'appelle le proxy "local" est en fait un port-forward classique, équivalent en ssh à ssh -L, permettant de mapper sur un port local une connexion distante. Côté Kubernetes, via l'outil kubectl, il s'agit de la commande kubectl port-forward, permettant de mapper un port d'un service distant en local afin que vous puissiez y accéder depuis votre machine.

Pour faire cela, Monday utilise simplement la commande ssh pour le forward SSH et pour Kubernetes, nous utilisons le client Go Kubernetes :

request := restClient.Post().Resource("pods").Namespace(namespace).Name(pod.Name).SubResource("portforward")
url := url.URL{
    Scheme:   request.URL().Scheme,
    Host:     request.URL().Host,
    Path:     buildPath(request),
    RawQuery: "timeout=30s",
}

transport, upgrader, _ := spdy.RoundTripperFor(f.clientConfig)
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", &url)

l := NewLogstreamer(pod.Name)
stopChannel := make(chan struct{}, 1)
readyChannel := make(chan struct{})

fw, _ := portforward.New(dialer, ports, stopChannel, readyChannel, l, l)

err = fw.ForwardPorts()

À partir d'une resource, il vous permet en effet de lancer facilement un port-forward en local et vous offre également la possibilité d'utiliser deux channels readyChannel vous permettant d'identifier lorsque le forward est fonctionnel et stopChannel lorsque la connexion est rompue: vous pouvez ainsi facilement re-lancer la connexion.

Forwarder : Comment fonctionne le proxy "remote" ?

Maintenant, un peu plus complexe : je vais décrire la façon avec laquelle nous effectuons du forward de type "remote" : il s'agit donc du fait de transmettre le traffic entrant d'un service Kubernetes vers votre machine en local. Il y a deux notions que nous allons utiliser et qui sont à connaitre pour cela :

  • Le SSH remote-forwaard (ssh -R), à l'inverse du local-forward (ssh -L), il permet de diriger le traffic du remote vers la connexion cliente, pour un port donné,
  • Pour pouvoir faire ce remote-forward SSH, nous allons déployer l'image proxy https://github.com/eko/monday/blob/master/docker-proxy/Dockerfile sur votre service Kubernetes.

Voici le principe du remote-forward vers Kubernetes :

Monday - Remote forward

L'idée est essentiellement de mettre à jour le déploiement de Kubernetes pour définir l'image ekofr/monday-proxy Docker qui ouvrira le port SSH et vous permettra de le transférer localement. Une fois qu'il est disponible localement, Monday n'a plus qu'à exécuter une commande SSH remote-forward.

Enfin, ce qui est vraiment cool avec le client Kubernetes Go, c'est qu'il expose un grand nombre d'interfaces et qu'il est donc facile de mocker le client Kubernetes Go dans les tests unitaires. En voici un aperçu :

deploymentInterface := &clientmocks.DeploymentInterface{}
deploymentInterface.On("List", metav1.ListOptions{LabelSelector: "app=my-test-app"}).
    Return(&appsv1.DeploymentList{
        Items: []appsv1.Deployment{},
    })

appsV1Interface := &clientmocks.AppsV1Interface{}
appsV1Interface.On("Deployments", "backend").
    Return(deploymentInterface)

clientSetMock := &clientmocks.Interface{}
clientSetMock.On("AppsV1").
    Return(appsV1Interface)

Conclusion

Créer cet outil a été très amusant pour moi car j'ai appris de nouvelles choses sur la gestion de processus unix avec Go et aussi sur le client Kubernetes Go. Je serais très heureux si vous voyez un intérêt à adopter Monday dans vos équipes de développement et me faire des feedbacks sur vos besoins.

N'hésitez pas à prendre contact !