Je sais le titre est long, mais vous ne serez pas déçu ! Pour mettre en oeuvre cet exemple je vais mettre en oeuvre une stack Docker dans mon cluster Docker Swarm. Je vais contruire toutes les images des containers Docker de cet exemple d’application REST développée en Go.
Le fonctionnement de l’architecture suit ce schéma.
Pour créer ce container Docker, je m’appuie sur la distribution Linux Alpine, qui permet d’obtenir des images de petite taille.
FROM alpine
COPY run.sh /root
RUN set -x && \
chmod +x /root/run.sh && \
apk update && \
apk upgrade && \
apk add --no-cache mongodb && \
rm /usr/bin/mongoperf && \
rm -rf /var/cache/apk/*
VOLUME /data/db
EXPOSE 27017 28017
ENTRYPOINT [ "/root/run.sh" ]
CMD ["mongod"]
Le script sh permet principalement de démarrer le serveur NoSQL Mongo DB en mode non-root.
#!/bin/sh
# Docker entrypoint (pid 1), run as root
[ "$1" = "mongod" ] || exec "$@" || exit $?
# Make sure that database is owned by user mongodb
[ "$(stat -c %U /data/db)" = mongodb ] || chown -R mongodb /data/db
# Drop root privilege (no way back), exec provided command as user mongodb
cmd=exec; for i; do cmd="$cmd '$i'"; done
exec su -s /bin/sh -c "$cmd" mongodb
Gorilla Mux est une librairie qui permet de créer des routes pour l’API Restful. Je vais créer 6 routes (voir à la fin du code source ci-dessous), une pour connaitre le status de mon application back-app et 5 pour mettre en oeuvre le CRUD (Create, ReadOne, ReadMany, Update, Delete). Chaque route est associée à une méthode HTTP et pointe vers une fonction, qui va agir sur la base de données MongoDB.
Mon application Golang, démarre et écoute sur le port 3000.
package main
import (
"os"
"encoding/json"
"log"
"net/http"
"gopkg.in/mgo.v2/bson"
"github.com/gorilla/mux"
. "github.com/user/app/config"
. "github.com/user/app/dao"
. "github.com/user/app/models"
)
var config = Config{}
var dao = ContactsDAO{}
// GET list of contacts
func AllContacts(w http.ResponseWriter, r *http.Request) {
contacts, err := dao.FindAll()
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
retResponse(w, http.StatusOK, contacts)
}
// GET a contact by its ID
func FindContactEndpoint(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
contact, err := dao.FindById(params["id"])
if err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid Contact ID")
return
}
retResponse(w, http.StatusOK, contact)
}
// POST a new contact
func CreateContact(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var contact Contact
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
contact.ID = bson.NewObjectId()
if err := dao.Insert(contact); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
retResponse(w, http.StatusCreated, contact)
}
// PUT update an existing contact
func UpdateContact(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var contact Contact
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := dao.Update(contact); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
retResponse(w, http.StatusOK, map[string]string{"result": "success"})
}
// DELETE an existing contact
func DeleteContact(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var contact Contact
if err := json.NewDecoder(r.Body).Decode(&contact); err != nil {
respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := dao.Delete(contact); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
retResponse(w, http.StatusOK, map[string]string{"result": "success"})
}
func StatusContact(w http.ResponseWriter, r *http.Request) {
name, err := os.Hostname()
if err != nil {
panic(err)
}
retResponse(w, http.StatusOK, map[string]string{"server":name,"result": "success"})
}
func respondWithError(w http.ResponseWriter, code int, msg string) {
retResponse(w, code, map[string]string{"error": msg})
}
func retResponse(w http.ResponseWriter, code int, payload interface{}) {
response, _ := json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}
// Parse the configuration file 'config.toml', and establish a connection to DB
func init() {
config.Read()
dao.Server = config.Server
dao.Database = config.Database
dao.Connect()
}
// Define HTTP request routes
func main() {
r := mux.NewRouter()
r.HandleFunc("/app-back-status", StatusContact)
r.HandleFunc("/contacts", AllContacts).Methods("GET")
r.HandleFunc("/contacts", CreateContact).Methods("POST")
r.HandleFunc("/contacts", UpdateContact).Methods("PUT")
r.HandleFunc("/contacts", DeleteContact).Methods("DELETE")
r.HandleFunc("/contacts/{id}", FindContactEndpoint).Methods("GET")
if err := http.ListenAndServe(":3000", r); err != nil {
log.Fatal(err)
}
}
Pour les détails sur la compréhension de ce fichier Dockerfile, veuillez consulter mon article précédent Comment utiliser Docker multi-stage build avec Golang.
FROM golang as builder
WORKDIR /go/src/github.com/user/app
COPY . .
RUN set -x && \
go get -d -v . && \
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM scratch
WORKDIR /root/
COPY --from=builder /go/src/github.com/user/app .
CMD ["./app"]
Attention à ce stade, l’ensemble des fichers certificats SSL est incorporé dans l’image Docker !
FROM alpine:edge
RUN set -x \
&& apk update \
&& apk upgrade \
&& apk add --no-cache nginx inotify-tools openssl
RUN openssl req -x509 -nodes \
-days 365 \
-newkey rsa:2048 \
-keyout server.key \
-out server.crt \
-subj "/C=FR/ST=Aquitaine/L=Bordeaux/O=MeMyselfAndI/OU=IT Department/CN=webapp.local" \
&& openssl dhparam -out dhparam.pem 2048 \
&& mkdir /etc/certs \
&& mv server.* /etc/certs \
&& mv dhparam.pem /etc/certs \
&& apk del openssl \
&& rm -rf /var/cache/apk/*
COPY nginx.conf /etc/nginx/nginx.conf
COPY reload.sh /
RUN chmod +x reload.sh
RUN mkdir -p /run/nginx
EXPOSE 80 443
CMD ["/reload.sh"]
Au cours de la génération de l’image Docker Nginx, la commande openssl est exécutée pour créer le certificat SSL.
Dans le ficher Dockerfile, inotify surveille si le certificat SSL est mis à jour.
#!/bin/sh
nginx -g "daemon off;" &
while true
do
inotifywait -e create -e modify /etc/certs /etc/nginx/conf.d/
nginx -t
if [ $? -eq 0 ]
then
echo "Reloading Nginx Configuration"
nginx -s reload
fi
done
3 choses sont à noter dans le fichier de configuration Nginx :
J’ai activé le protocole http2, mais pour une raison que je ne connais pas, l’équipe Alpine Linux n’a pas activé cette option dans le package Nginx.
server {
listen 80;
listen [::]:80;
location / {
if ($scheme = http) {
return 301 https://$host$request_uri;
}
}
}
server {
listen 443 http2 ssl default_server;
listen [::]:443 http2 ssl default_server;
server_name nginx;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers On;
ssl_certificate /etc/certs/server.crt;
ssl_certificate_key /etc/certs/server.key;
ssl_dhparam /etc/certs/dhparam.pem;
ssl_session_cache shared:SSL:128m;
add_header Strict-Transport-Security "max-age=31557601; includeSubDomains";
ssl_stapling on;
ssl_stapling_verify on;
# Your favorite resolver may be used instead of the Google one below
resolver 8.8.4.4 8.8.8.8 valid=300s;
resolver_timeout 10s;
root /var/www;
index index.html;
location /nginx-status {
default_type application/json;
return 200 '{"status":"200", "message": "Healthcheck OK"}';
}
location ~^/(contacts|app-back-status) {
proxy_pass http://app-back:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Voici le fichier de composition docker-stack.yml qui va me permettre de déployer nos services Docker, il est composé de 3 services :
Je crée 2 networks :
version: "3"
services:
app-back:
image: itwars/mygo
networks:
- mongo-go
- nginx-go
ports:
- 3000:3000
depends_on:
- mongodb
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
nginx:
image: itwars/nginx-http2
volumes:
- /home/vrh/docker/myrepo/stack-mongo-golang-vuejs-nginx/nginx-http2/conf/:/etc/nginx/conf.d/
ports:
- 80:80
- 443:443
networks:
- nginx-go
depends_on:
- app-back
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
mongodb:
image: itwars/mongodb
volumes:
- mongodb-data:/data/db
networks:
- mongo-go
ports:
- 27017:27017
- 28017:28017
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
networks:
mongo-go:
nginx-go:
volumes:
mongodb-data:
Pour démarrer la stack de services Docker dans un cluster Docker Swarm, j’utilise la commande suivante :
docker stack deploy -c docker-stack.yml myapp
Attention: si vous démarrer cette stack Docker dans un cluster Docker Swarm, il faut au préalable que les images Docker aient été pushées sur Docker Hub.
curl -k -H "Content-Type: application/json" https://192.168.1.24/app-back-status 2>/dev/null | jq
Voici la réponse de l’application Golang :
{
"result": "success",
"server": "50f0ead2e935"
}
Pour ajouter un nouvel enregistrement dans la base de données MongoDB :
curl -k -d '{"nom":"RABAH", "prenom":"Vincent", "telephone":"0000000"}' -H "Content-Type: application/json" -X POST https://192.168.1.24/contacts
La réponse du serveur :
{
"id":"59e65c15e34ad00001c19cf7",
"prenom":"Vincent",
"nom":"RABAH",
"telephone":"0000000"
}
Maintenant, je vais scaler le service app-back et vérifier que mes requêtes sont bien distribuées dans le cluster Docker Swarm
docker service scale test_app-back=3
test_app-back scaled to 3
Maintenant si j’exécute la commande suivante 4 fois, on constate que ma requête est bien distribuée sur les 3 instances de mon application Golang :
curl -k -H "Content-Type: application/json" https://192.168.1.24/app-back-status 2>/dev/null | jq
Le résultat de 4 requêtes :
{
"result": "success",
"server": "0c07a393b077"
}
{
"result": "success",
"server": "c01f9d78eb68"
}
{
"result": "success",
"server": "50f0ead2e935"
}
{
"result": "success",
"server": "0c07a393b077"
}
J’espère que ce tuto, vous permettra d’avancer comme moi. Personnellement, il contribue à me faire progresser un peu en Golang et à tout “Dockeriser”.
N’hésitez pas à me laisser un commentaire et à ajouter une étoile sur mon repo GitHub. Merci.