Capensis.fr
Support professionnel

Comment déployer une API REST Go avec Docker ?

docker
#1

C’est un fait, le langage Go (aussi noté Golang) est désormais de plus en plus présent dans les projets Open Source. Performant et portable, le Go permet de développer rapidement des applications en tout genre mais également des API, REST de préférence! A l’heure des micro-services, ce serait bien dommage de surfer sur cette tendance tout en déployant vos projets à l’ancienne! Découvrons ensemble comment déployer vos API Go avec Docker.

Nous partirons d’une API REST la plus simple possible pour arriver, étape par étape, à une API structurée utilisant des briques logiciels externes (Ex. Base de données) comme dans la vraie vie!

Installer Docker

La version de Docker présente dans les dépôts officiels de votre distribution n’est pas forcément la plus récente, sous Linux, vous pouvez donc procéder à l’installation avec le script officiel de Docker (à lancer en root) :

curl -sSL https://get.docker.com/ | sh

Le code de l’API

Pour démarrer en douceur, prenons un exemple simple d’une API REST qui renvoi un petit JSON.

Créons un fichier main.go :

package main

import (
	"fmt"
	"log"
	"net/http"
)

func home(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("content-type", "application/json")
	fmt.Fprintln(w, `{"message": "Bonjour tout le monde!"}`)
}

func ping(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, `PONG`)
}

func main() {
	// définition des routes
	http.HandleFunc("/", home)
	http.HandleFunc("/PING", ping)

	// démarrage de l'écoute avec log en cas d'erreur
	log.Fatal(http.ListenAndServe(":8001", nil))
}

Le paquet HTTP nous permet ici de créer deux routes ("/" et “PING”) et de les servir sur le port 8001 grâce au serveur Web intégré.

Si vous avez déjà Go d’installé sur votre poste, vous pouvez tester directement en ligne de commande :

go run main.go

Et dans un autre terminal, appelons l’API avec la commande curl (avec un navigateur web, ça fonctionne aussi) :

curl http://localhost:8001/

La sortie attendue :

{"message": "Bonjour tout le monde!"}

Si vous n’avez pas go, ne perdez pas de temps à l’installer directement sur votre poste, passez au déploiement avec Docker.

Définition de l’image Docker

Avec Docker, tout part des images, et ça tombe bien, il en existe une officielle pour Go, il nous suffit donc de partir de cette image en indiquant la version go souhaitée (ici 1.12), de copier notre code et de l’exécuter.

Créons un fichier nommé Dockerfile :

FROM golang:1.12

WORKDIR /go/src/app
COPY main.go .

RUN go install -v ./...

CMD ["app"]

L’instruction RUN permet de lancer la commande go install au moment de la construction de l’image afin de compiler le code. L’instruction CMD quant à elle, permet d’exécuter le code au moment de l’exécution du conteneur.

Déploiement

Pour déployer notre API Go, nous devons construire l’image grâce à notre Dockerfile et lancer un conteneur Docker à partir de cette image.

Lançons la construction de l’image en spécifiant un tag “mon_api_go” :

docker build -t mon_api_go .

Puis, lançons un conteneur nommé “api_go” avec l’image fraîchement construite :

docker run -dit --name api_go -p 8001:8001 mon_api_go

Pour résumer le rôle des paramètres :
-d : mode détaché, sinon la commande ne rend pas la main
-i et -t : mode interactif pour garder un STDIN ouvert et pour obtenir la sortie standard afin de pouvoir prendre la main par la suite sur notre conteneur
-p : exposition d’un port du conteneur sur l’hôte

Enfin, nous pouvons vérifier que l’API fonctionne, depuis notre poste :

curl http://localhost:8001/

Dans le cas où la sortie ne correspond pas au JSON vu plus haut, nous pouvons obtenir les logs du conteneur avec la commande logs:

docker logs -f api_go

Le -f permet ici de suivre les changements en temps réel.

Avec des dépendances

Pour construire une API plus complexe, vous allez très certainement utiliser des librairies existantes. La première qui est le plus souvent judicieux d’inclure est un framework comme mux qui va nous permettre de construire votre API avec un cadre déjà bien pensé.

Faisons évoluer notre API en créant l’arborescence suivante :

src
    handlers.go
    main.go
    routes.go    
Dockerfile

main.go

package main

import (
	"log"
	"net/http"
)

func main() {
	// instanciation du router
	r := NewRouter()

	// démarrage de l'écoute avec log en cas d'erreur
	log.Fatal(http.ListenAndServe(":8001", r))
}

routes.go

package main

import (
	"github.com/gorilla/mux"
)

func NewRouter() *mux.Router {
    // déclaration des routes
	r := mux.NewRouter()
	r.HandleFunc("/", home)
	r.HandleFunc("/PING", ping)

	return r
}

handlers.go

package main

import (
	"fmt"
	"net/http"
)

func home(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("content-type", "application/json")
	fmt.Fprintln(w, `{"message": "Bonjour tout le monde!"}`)
}

func ping(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, `PONG`)
}

Maintenant que notre code source est éclaté en plusieurs fichiers et que nous utilisons des dépendances, nous devons faire évoluer notre Dockerfile comme suit :

FROM golang:1.12

WORKDIR /go/src/app

# copie tout le répertoire src
COPY src .

# récupération des dépendances
RUN go get -d -v ./...

# compilation
RUN go install -v ./...

CMD ["app"]

A noter ici que nous avons copier tout le répertoire src et nous avons lancé la commande go get pour récupérer les dépendances de notre projet.

Nous pouvons reconstruire notre image et recréer notre conteneur.

docker stop api_go
docker rm api_go
docker build -t mon_api_go .
docker run -dit --name api_go -p 8001:8001 mon_api_go

Et nous testons depuis notre poste de la même manière notre API :

curl http://localhost:8001/

Avec une base de données

Nous avons jusqu’ici lancé un seul conteneur avec notre API REST mais dans la plupart des cas, nous aurons besoin de briques logiciels diverses et la plus courante est sans conteste … la base de données! Pour illustrer le déploiement d’une API REST avec une base de données, nous allons faire évoluer notre API go et ajouter un verbe (ou une route) qui incrémentera un compteur et retournera la nouvelle valeur. Pour faire ce genre de choses, Redis est un excellent candidat.

Nous allons donc devoir déployer plusieurs conteneurs mais heureusement, les développeurs de Docker ont tout prévu et nous proposent un formidable outil à savoir Docker Compose qui va nous permettre de définir les conteneurs nécessaires à notre application.

Pour Installer Docker compose sous Linux (existe aussi sous Windows et Mac) :

sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Maintenant que Docker compose est installé, nous pouvons définir les conteneurs que nous avons besoin en créant un fichier docker-compose.yml :

version: '3'
services:
   redis:
     image: redis:5.0
   api_go:
     build: .
     depends_on:
       - redis
     environment:
       REDIS_HOST: redis
       REDIS_PORT: 6379
     ports:
       - "8002:8001"

Nous indiquons ici que nous avons besoin de deux conteneurs Docker, un permier nommé “redis” qui sera un conteneur basé sur l’image “redis:5.0” et un second qui sera basé sur une image à construire avec le Dockerfile du répertoire courant (directive build: .).

Nous spécifions également que le conteneur “api_go” ne doit être lancé que si le conteneur “redis” l’est (directive depends_on).

Enfin, nous définissons des variables d’environnement “REDIS_HOST” et “REDIS_PORT” afin de pouvoir joindre le service Redis depuis le conteneur de notre API.

Adaptons maintenant le code de l’API Go en modifiant handlers.go comme suit :

package main

import (
	"fmt"
	"github.com/go-redis/redis"
	"net/http"
	"os"
)

func home(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("content-type", "application/json")
	fmt.Fprintln(w, `{"message": "Bonjour tout le monde!"}`)
}

func ping(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, `PONG`)
}

func inc(w http.ResponseWriter, r *http.Request) {
	redisdb := redis.NewClient(&redis.Options{
		Addr:     os.Getenv("REDIS_HOST") + ":" + os.Getenv("REDIS_PORT"),
		Password: "",
		DB:       0,
	})

	result, err := redisdb.Incr("counter").Result()
	if err != nil {
		panic(err)
	}

	fmt.Fprintln(w, result)
}

Et ajoutons une route dans routes.go:

package main

import (
	"github.com/gorilla/mux"
)

func NewRouter() *mux.Router {
	r := mux.NewRouter()
	r.HandleFunc("/", home)
	r.HandleFunc("/PING", ping)
	r.HandleFunc("/INC", inc)

	return r
}

Et c’est tout!

Déploiement avec Docker Compose

Maintenant que notre API est prête à incrémenter le compteur et que nous avons notre fichier docker-compose.yml, nous pouvons lancer le déploiement du service :

docker-compose up --build -d

Le paramètre --build permet ici de forcer la construction de l’image, même si l’image est présente localement et -d permet de lancer la commande en mode détaché.

Par défaut, la commande utilise le fichier docker-compose.yml du répertoire courant et préfixe le nom des conteneurs avec le nom du répertoire.

Pour en avoir le cœur net, vous pouvez afficher les conteneurs créer pour ce service :

docker-compose ps

Exemple de sortie :

         Name                   Command           State           Ports         
--------------------------------------------------------------------------------
projet_go_docker_api_go_1   app                      Up  0.0.0.0:8002->8001/tcp
projet_go_docker_redis_1    docker-entrypoint.sh...  Up    6379/tcp               

Vous pouvez également consulter les logs du service (logs de tous les conteneurs du service) :

docker-compose logs -f 

Comme nous avons exposé le port 8001 du conteneur sur le port 8002 de l’hôte dans le fichier docker-compose.yml, nous pouvons appeler directement l’API comme suit:

curl http://localhost:8002/INC
1
curl http://localhost:8002/INC
2
curl http://localhost:8002/INC
3

Si vous modifiez le code de l’API, vous devez stopper le service et le relancer (toujours en se positionnant dans le répertoire où il y a le fichier docker-compose.yml) :

docker-compose stop
docker-compose up --build -d

Conclusion

J’espère que cette brève introduction à Docker dans le cadre d’un développement Go vous aura permis d’apprécier les opportunités et le confort apporté par ces technologies et vous aura donné envie de déployer du conteneurs!

1 Like