Skip to main content

5 posts tagged with "docker"

View All Tags

· 9 min read
TheBidouilleur

Dagger.io est un projet qui a été annoncé il y a quelque temps par Solomon Hykes, la philosophie de Dagger a attiré mon attention.

C'est un service de CI/CD qui permet de lancer des jobs dans des conteneurs Docker. La plus-value de Dagger est qu'il ne se limite pas à du Yaml (Comme Gitlab-CI, Github Action, Drone.io) ou à un DSL maison (Comme Jenkins), il permet de lancer des jobs en utilisant du code Python, du Go, du Java.Typescript ou encore du GraphQL.

Il est un peu comme Pulumi mais pour les jobs de CI/CD. (Là où son concurrent Terraform utilise un DSL, Pulumi utilise le Typescript, Python, Java, etc)

Étant donné que j'utilise Github pour mes projets publics, Gitea pour mes projets privés (couplé à Drone) et Gitlab pour les projets professionnels, je me suis dit que c'était l'occasion de tester Dagger.io et de me débarrasser de mes fichiers Yaml ayant une syntaxe différente en fonction de la plateforme.

Mon idée derrière la conversion de mes jobs de CI/CD en code est également d'avoir les mêmes résultats entre les différentes plateformes et ma machine locale.

On va donc faire le point sur ce qu'est Dagger.io, comment l'installer et comment l'utiliser. Comme je suis habitué au langage Python, j'utiliserai alors le SDK Python de Dagger.io !

Installation de Dagger.io

Il sera nécessaire d'avoir un Python 3.10 ou supérieur pour utiliser Dagger.io (il est aussi possible d'utiliser un venv).

Pour installer Dagger.io, il n'y a rien de bien compliqué, il suffit d'installer le package via pip.

pip install dagger-io

Et c'est terminé pour l'installation.

ERROR: Could not find a version that satisfies the requirement dagger-io (from versions: none)

Si vous avez une erreur de ce type :

➜  ~ python3 -m pip install dagger-io 
Defaulting to user installation because normal site-packages is not writeable
Collecting dagger-io
Using cached dagger_io-0.4.2-py3-none-any.whl (52 kB)
Collecting cattrs>=22.2.0
[...]
Using cached mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Collecting multidict>=4.0
Using cached multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (114 kB)
ERROR: Exception:
Traceback (most recent call last):
File "/usr/lib/python3/dist-packages/pip/_internal/cli/base_command.py", line 165, in exc_logging_wrapper
status = run_func(*args)
File "/usr/lib/python3/dist-packages/pip/_internal/cli/req_command.py", line 205, in wrapper
return func(self, options, args)
File "/usr/lib/python3/dist-packages/pip/_internal/commands/install.py", line 389, in run
to_install = resolver.get_installation_order(requirement_set)
File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/resolver.py", line 188, in get_installation_order
weights = get_topological_weights(
File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/resolver.py", line 276, in get_topological_weights
assert len(weights) == expected_node_count
AssertionError

Il se peut que vous ayez une version trop ancienne de pip et setuptools. La solution est de mettre à jour pip et setuptools via la commande suivante :

pip install --upgrade pip setuptools

Si vous ne souhaitez pas travailler avec l'utilisateur root, il vous faudra configurer le mode Rootless de Docker. (C'est ce que j'ai fait) Pour cela, il suffit de suivre la documentation officielle.

Premier job

Pour commencer, nous allons créer un fichier hello-world.py et y ajouter le code suivant :

"""Execute a command."""
import sys
import anyio
import dagger

async def test():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
python = (
client.container()
.from_("python:3.11-slim-buster")
.with_exec(["python", "-V"])
)
version = await python.stdout()
print(f"Hello from Dagger and {version}")

if __name__ == "__main__":
anyio.run(test)

Il s'agit d'un simple job qui va lancer un conteneur Docker avec l'image python:3.11-slim-buster et exécuter la commande python -V.

Pour lancer le job, il suffit de lancer avec python : python3 hello-world.py.

➜  python3 hello-world.py    
#1 resolve image config for docker.io/library/python:3.11-slim-buster
#1 DONE 1.7s
#2 importing cache manifest from dagger:10686922502337221602
#2 DONE 0.0s
#3 DONE 0.0s
#4 from python:3.11-slim-buster
#4 resolve docker.io/library/python:3.11-slim-buster
#4 resolve docker.io/library/python:3.11-slim-buster 0.2s done
#4 sha256:f0712d0bdb159c54d5bdce952fbb72c5a5d2a4399654d7f55b004d9fc01e189e 0B / 3.37MB 0.2s
#4 sha256:f0712d0bdb159c54d5bdce952fbb72c5a5d2a4399654d7f55b004d9fc01e189e 3.37MB / 3.37MB 0.3s done
#4 extracting sha256:80384e04044fa9b6493f2c9012fd1aa7035ab741147248930b5a2b72136198b1
#4 extracting sha256:80384e04044fa9b6493f2c9012fd1aa7035ab741147248930b5a2b72136198b1 0.3s done
#4 extracting sha256:f0712d0bdb159c54d5bdce952fbb72c5a5d2a4399654d7f55b004d9fc01e189e
#4 extracting sha256:f0712d0bdb159c54d5bdce952fbb72c5a5d2a4399654d7f55b004d9fc01e189e 0.2s done
#4 ...
#3
#3 0.224 Python 3.11.2
#3 DONE 0.3s

#4 from python:3.11-slim-buster
Hello from Dagger and Python 3.11.2

Félicitations, vous avez lancé votre premier job avec Dagger.io !

Maintenant, nous allons voir comment créer un script un peu plus complexe !

Dagger, Python et Docker

Jusque-là, nous n'avons pas beaucoup profité de la puissance de Python, ou même des fonctionnalités de Docker. Nous allons donc voir comment utiliser les deux ensemble.

Vous n'êtes pas sans savoir que j'utilise Docusaurus pour générer le code HTML que vous visionnez en ce moment même. Docusaurus me permet d'écrire mes articles en Markdown et de les transformer en site.

N'étant pas très regardant sur la qualité de mes Markdown, j'ai décidé de créer un job qui va vérifier la syntaxe de mes fichiers Markdown et me renvoyer une erreur s'il y a un problème sur l'un d'entre eux.

Pour cela, je vais utiliser pymarkdownlnt, un Linter assez strict et performant.

Son installation se fait via pip :

pip install pymarkdownlnt

Ainsi, notre job va devoir effectuer ces étapes de manière séquentielle :

  • Démarrer à partir d'une image Python (FROM python:3.10-slim-buster)
  • Installer pymarkdownlnt (RUN pip install pymarkdownlnt)
  • Récupérer les fichiers du projet (COPY . .)
  • Lancer le linter sur les fichiers Markdown de chaque dossier blog/ docs/ i18n/ (RUN pymarkdownlnt scan blog/-r)

Nous pouvons traduire les 3 premières étapes en code Python :

lint = (
client.container().from_("python:3.10-slim-buster")
.with_exec("pip install pymarkdownlnt".split(" "))
.with_mounted_directory("/data", src)
.with_workdir("/data")
)

Et ensuite… je souhaite faire une boucle itérant sur les dossiers blog/ docs/ i18n/ et lancer le linter sur chacun d'entre eux. C'est à ce moment précis que nous allons utiliser du Python et plus uniquement des instructions Dagger.

Un détail que je ne vous ai pas encore mentionné, c'est que nous pouvons agir sur notre job tant qu'il n'est pas lancé, c'est-à-dire avant le await qui va attendre la fin de l'exécution du job.

Donc… gardons la définition du conteneur ci-dessus, et ajoutons 3 tâches à notre job :

for i in ["blog", "docs", "i18n"]:
lint = lint.with_exec(["pymarkdownlnt", "scan", i, "-r"])

Plutôt simple, non ?

Si je lance mon job, j'ai de nombreuses erreurs à propos de règles que je n'ai pas respectées. Mais c'est normal, la syntaxe de Docusaurus cause des erreurs dans le linter que je ne peux pas corriger.

Je vais donc noter les règles qui ne s'appliquent pas à mes fichiers, et les ignorer :

lint_rules_to_ignore = ["MD013","MD003","MD041","MD022","MD023","MD033","MD019"]
# Format accepté par pymarkdownlint : "MD013,MD003,MD041,MD022,MD023,MD033,MD019"
for i in ["blog", "docs", "i18n"]:
lint = lint.with_exec(["pymarkdownlnt", "-d", str(','.join(lint_rules_to_ignore)), "scan", i, "-r"])

Voici notre script complet :

"""Markdown linting script."""
import sys
import anyio
import dagger
import threading

async def markdown_lint():
lint_rules_to_ignore = ["MD013","MD003","MD041","MD022","MD023","MD033","MD019"]

async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
src = client.host().directory("./")

lint = (
client.container().from_("python:3.10-slim-buster")
.with_exec("pip install pymarkdownlnt".split(" "))
.with_mounted_directory("/data", src)
.with_workdir("/data")
)

for i in ["blog", "docs", "i18n"]:
lint = lint.with_exec(["pymarkdownlnt", "-d", str(','.join(lint_rules_to_ignore)), "scan", i, "-r"])
# execute
await lint.stdout()
print(f"Markdown lint is FINISHED!")

if __name__ == "__main__":
try:
anyio.run(markdown_lint)
except:
print("Error in Linting")

Après cette modification, mon job fonctionne sans problème !

python3 .ci/markdown_lint.py

Récapitulons ce que nous savons faire :

  • Lancer une image Docker
  • Exécuter des commandes dans un conteneur
  • Copier des fichiers depuis l'hôte vers le conteneur

Je pense que ça suffira dans la plupart de mes CI. Néanmoins, il reste une fonctionnalité qui me manque : la possibilité de construire une image Docker et de l'envoyer sur un registre.

Build & push d'une image Docker

Il est possible de s'authentifier sur un registre directement via Dagger. Dans mon cas, je considère que l'hôte sur lequel je lance mon job est déjà authentifié.

Dans le cadre de cette démonstration, je vais utiliser le registre ttl.sh, un registre public et anonyme permettant justement de stocker des images Docker pendant une durée maximale de 24h.

async def docker_image_build():
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
src = client.host().directory("./")
build = (
client.container()
.build(
context = src,
dockerfile = "Dockerfile",
build_args=[
dagger.BuildArg("APP", os.environ.get("APP", "TheBidouilleurxyz"))
]
)
)
image = await blog.build(address="ttl.sh/thebidouilleur:1h")

Le code ci-dessus va donc construire mon image Docker à partir du fichier Dockerfile présent dans le dossier courant, et l'envoyer sur le registre ttl.sh/thebidouilleur:1h.

Une petite particularité de ce code est l'usage de Build Args. J'utilise la variable d'environnement APP, si cette variable n'est pas définie, je vais récupérer la valeur par défaut TheBidouilleurxyz.

Maintenant, je souhaite créer un job similaire qui va construire une image Docker multiarchitecture ARM et AMD64 (l'un de mes clusters Kubernetes est composé de Raspberry Pi).

Build & push d'une image Docker multiarchitecture

Il faudra déjà mettre au point le build multiarchitecture sur votre machine avant de pouvoir l'intégrer à notre job Dagger.

Si vous souhaitez savoir comment créer une image Docker multiarchitecture, je vous invite à lire ma documentation Création image Docker pour en connaitre la procédure.

On va utiliser un objet à mettre en paramètre à Dagger, celui-ci est dagger.Platform et permet de spécifier la plateforme sur laquelle on veut construire notre image Docker.

Nous créons une boucle qui va itérer sur les différentes architectures avec lesquelles on veut construire notre image, et lors du Publish, nous enverrons les différentes images construites.

async def docker_image_build():
platforms = ["linux/amd64", "linux/arm64"]
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as client:
src = client.host().directory(".")
variants = []
for platform in platforms:
print(f"Building for {platform}")
platform = dagger.Platform(platform)
build = (
client.container(platform=platform)
.build(
context = src,
dockerfile = "Dockerfile"
)
)
variants.append(build)
await client.container().publish("ttl.sh/dagger_test:1h", platform_variants=variants)

Docker avec plusieurs architectures

Créer un lanceur

Maintenant que nous avons vu comment utiliser Dagger, nous allons créer un lanceur qui va nous permettre de lancer nos jobs un-par-un.

Pour lancer nos taches en asynchrone, nous utilisons la librairie anyio sur chacun de nos scripts.

import anyio

import markdown_lint
import docusaurus_build
import multi_arch_build as docker_build

if __name__ == "__main__":

print("Running tests in parallel using anyio")
anyio.run(markdown_lint.markdown_lint)
anyio.run(docusaurus_build.docusaurus_build)
anyio.run(docker_build.docker_build)

Ce lanceur va importer les méthodes des fonctions markdown_lint, docusaurus_build et docker_build des fichiers markdown_lint.py, docusaurus_build.py et multi_arch_build.py avant d'exécuter chacune de ces fonctions.

L'unique intérêt de ce lanceur est de pouvoir lancer nos jobs à partir d'une seule commande.

Conclusion

Dagger est un produit très prometteur ! Celui-ci n'arrivera surement pas à remplacer les solutions actuelles telles que Github Actions ou Gitlab CI, mais il répond à un besoin spécifique : celui d'avoir le même CI peu importe la plateforme.

Bref, Dagger est un produit qui mérite d'être testé et je pense que je vais l'utiliser pour la plupart de mes projets personnels.

J'espère que cet article vous aura plu, n'hésitez pas à me faire part de vos retours.

· 6 min read
TheBidouilleur

Since the DevOps movement started (or rather Platform engineering), the topic of high availability has been brought to the forefront. And one of the most versatile solutions to achieve high availability is to create application clusters. (and so: containers)

So I've been running a Swarm cluster for a few years and I recently switched to Kubernetes (k3s to be precise). And by having clusters holding several hundreds of containers, we forget about maintenance and update.

And in this article, we will talk about updates.

Out-of-cluster container upgrade solutions

WatchTower

I think the best known solution is Watchtower

Watchtower is easy to use and is based (like many others) on labels. A label allows to define some parameters and to activate (or deactivate) the monitoring of updates.

Updating is not always good...

Be careful not to automatically update sensitive programs! We can't check what an update and if they won't break something. It's up to you to choose which applications to monitor, and to trigger an update or not.

WatchTower will notify you in several ways:

  • email
  • slack
  • msteams
  • gotify
  • shoutrrr

And among these methods, you do not have only proprietary solutions, free to you to host a shoutrrr, a gotify or to use your smtp so that this information does not leave your IS! *(I am very critical of the use of msteams, slack, discord to receive notifications)

WatchTower will scan for updates on a regular basis (configurable).

container-updater (from @PAPAMICA)

The most provided/complex solution is not always the best. Papamica has set up a bash script to meet his specific needs (which many other people must have): an update system notifying him through Discord and Zabbix.

This one is also based on labels and also takes care of the case where you want to update by docker-compose. (instead of doing a docker pull, docker restart like Watchtower)

labels:
- "autoupdate=true"
- "autoupdate.docker-compose=/link/to/docker-compose.yml"

Even if I don't use it, I had a time when I was using Zabbix and I needed to be notified on my Zabbix. (which notified me by Mail/Gotify)

Papamica states that he plans to add private registry support (for now only github registry or dockerhub) as well as other notification methods.

Solutions for Swarm

Swarm is probably the container orchestrator I enjoyed the most: it's **simple**! You learn fast, you discover fast and you get quick results. But I've already written about Swarm in another article...

Sheperd

What I like in Papamica's program (and that goes with Sheperd) is that we keep bash as the central language. A language that we all know in the main thanks to Linux, and that we can read and modify if we take the time.

Shepherd's code is only ~200 lines and works fine like that.

version: "3"
services:
...
shepherd:
build: .
image: mazzolino/shepherd
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
placement:
constraints:
- node.role == manager

This one will accept several private registers, which gives a nice advantage compared to the other solutions presented. Example:

    deploy:
labels:
- shepherd.enable=true
- shepherd.auth.config=blog

Shepherd does not include a (default) notification system. That's why its creator decided to offer a Apprise sidecar as an alternative. Which can redirect to many things like Telegram, SMS, Gotify, Mail, Slack, msteams etc....

I think this is the simplest and most versatile solution. I hope it will be found in other contexts. I hope it will be found in other contexts (but I don't go into too much detail on the subject, I'd like to write an article about it).

I used Shepherd for a long time and I had no problems.

Solutions for Kubernetes

For Kubernetes, we start to lose in simplicity. Especially since with the imagePullPolicy: Always option, you just have to restart a pod to get the last image with the same tag. For a long time, I used ArgoCD to update my configurations and re-deploy my images at each update on Git.

But ArgoCD is only used to update the configuration and not the image. The methodology is incorrect and it is necessary to find a suitable tool for that.

Keel.sh

Keel is a tool that meets the same need: Update pod images. But it incorporates several features not found elsewhere.

Keel

If you want to keep the same operation as the alternatives (i.e. regularly check for updates), it is possible:

metadata:
annotations:
keel.sh/policy: force
keel.sh/trigger: poll
keel.sh/pollSchedule: "@every 3m"

But where Keel excels is that it offers triggers and approvals.

A trigger is an event that will trigger the update of Keel. We can imagine a webhook coming from Github, Dockerhub, Gitea which will trigger the update of the server. *(So we avoid a regular crontab and we save resources, traffic and time) As the use of webhook has become widespread in CICD systems, it can be coupled to many use cases.

The approvals are the little gem that was missing from the other tools. Indeed, I specified that updating images is dangerous and you should not target sensitive applications in automatic updates. And it's just in response to that that Keel developed the approvals.

Approval system of keel

The idea is to give permission to Keel to update the pod. We can choose the moment and check manually.

I think it's a pity that we have Slack or MSTeams imposed for the approvals, it's then a feature that I won't use.

A UI

So for now, I use Keel without its web interface, it may bring new features, but I would like to avoid an umpteenth interface to manage.

Conclusion

Updating a container is not that easy when you are looking for automation and security. If today, I find that Keel corresponds to my needs, I have the impression that the tools are similar without offering real innovations. (I'm thinking of tackling the canary idea one day) I hope to discover new solutions soon, hoping that they will better fit my needs.

· 10 min read
TheBidouilleur

L'année dernière, j'ai dit que j'appréciais particulièrement Caddy qui était simple, pratique, rapide et efficace. Caddy permet, à partir d'une ligne aussi simple que :

domain.tld {
reverse_proxy 127.0.0.1:80
}

En plus de ça, Caddy va constamment vérifier l'expiration de vos certificats letsencrypt et de les renouveler automatiquement sans aucune interaction nécéssaire. Caddy est également facile à déployer via Docker.

Que demander de plus ?

De l'automatisation ?

Parfaitement, cher lecteur ! Vous m'étonnez toujours ! J'ai donc créé un Rôle Ansible générant ma configuration automatiquement à partir d'un dépôt Git avec les IP correspondant aux domaines que je souhaite utiliser. Maintenant, à partir de ça, je peux faire un script Bash récupérant les ports de mes conteneurs, puis push sur mon Git les nouvelles redire….

C'est une usine à gaz…

Et vous avez raison ! Ce système est obsolète en quelques secondes lorsqu'on utilise un système de service discovery permettant de récupérer mes services et automatiser l'ajout de ces services sur mon g…. Bon d'accord, toujours "usine à gaz" !

Pas le choix, je vais devoir en conséquence remplacer Caddy par quelque chose d'autre. Et justement : je sais exactement le soft à utiliser.

Place à Traefik, le RP (Reverse proxy) multi-provider avec du service discovery.

Cet article sur Traefik est en cours de rédaction, vous pouvez me suivre sur twitter pour être au courant des prochaines écritures ainsi que mon avancement dans mes projets !

Qu'est-ce que Traefik ?

logo de traefik

Comme expliqué juste au-dessus, Traefik est un reverse-proxy qui se démarque des autres par son systeme de provider et de middleware. Il ne réinvente pas la roue, mais il est particulièrement efficace lorsque l'on a un grand nombre de redirections à paramétrer ou que nous avons des règles qui changent régulièrement.

si vous ignorez ce qu'est un reverse-proxy, je vous invite à consulter cet article de Ionos

Traefik n'est pas fait pour vous si :

  • Vous n'utilisez pas Docker, Kubernetes ou Consul
  • Si vous avez peu de règles (et surtout si elles sont statiques)
  • Vous ne vous souciez pas d'automatiser votre RP

et en revanche : Traefik est fait pour vous si :

  • Vos services sont répartis sur de nombreuses machines
  • Vous avez un Swarm / Kubernetes

Traefik, ce n'est pas pour tout le monde. Mais il y a de nombreux cas, et de nombreux domaines où Traefik n'est pas employé alors qu'il le devrait.

Comment fonctionne Traefik ?

Traefik se base sur un système de Provider. Un Provider est un moyen de récupérer les fameuses règles "domaine -> IP" de manière automatique (ou presque). Par exemple, sur Caddy, notre provider (la manière dont on récupère notre configuration) est un simple fichier. Notre seule manière d'automatiser Caddy se repose donc sur notre gestion de ce fichier. (le Caddyfile)

Et c'est justement cet unique provider qui va me faire pencher vers Traefik, qui possède une grande liste de provider. Parmis ces providers, nous avons :

  • Docker
  • Kubernetes / Rancher
  • Redis
  • des Fichiers classiques
  • Une API Json

et en fonction des providers que l'on accorde à Traefik(et du contenu), celui-ci va s'adapter pour créer les redirections de manière automatique.

Nous allons tester ça directement dans notre premier Traefik de test ! On va avant-tout créer le réseau Docker qui permettra à notre reverse-proxy d'accéder aux conteneurs.

docker network create --driver=overlay traefik-net

et on va créer notre docker-compose contenant Traefik:

version: "3.7"

services:
traefik:
image: "traefik:v2.5"
container_name: "traefik"
hostname: "traefik"
networks:
- traefik-net
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./config:/etc/traefik"
networks:
traefik-net:
external: true
driver: overlay
name: traefik-net

Puis, dans un dossier ./config, nous allons créer le fichier traefik.yml qui va contenir notre configuration, et nos providers.

# fichier traefik.yml, à mettre dans un dossier ./config
---
log:
level: "INFO"
format: "common"

providers:
docker:
endpoint: "unix:///var/run/docker.sock" # Provider Docker sur la machine locale
exposedByDefault: false # Par défaut, les conteneurs ne possèdent pas de redirection
network: "traefik-net" # Le réseau docker dans lequel il y aura.
watch: true
file:
filename: "/etc/traefik/dynamic.yml" # Fichier contenant les règles statiques
watch: true # Va actualiser son contenu régulièrement pour mettre les règles à jour
providersThrottleDuration: 10 # Va actualiser les règles chaque 10s

api: # Va rendre le Dashboard de Traefik accessible en http
dashboard: true
debug: false
insecure: true

entryPoints: # Notre entrée, nous acceptons les requetes via https sur le port 80
insecure:
address: ":80"

Et notre fichier dynamic.yml qui contiendra nos règles statiques :

http:
routers:
helloworld-http:
rule: "Host(`hello-world.tld`)"
service: hello-world
entryPoints:
- insecure

services:
hello-world:
loadBalancer:
servers:
- url: "http://192.168.128.1:80"

En démarrant Traefik, on on remarque qu'il va se mettre à jour chaque 10s en interrogeant le daemon Docker ainsi que le fichier.

On peut maintenant créer notre premier conteneur à rajouter de cette manière:

version: "3.7"

services:
whoami:
image: "containous/whoami"
container_name: "whoami"
hostname: "whoami"
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.entrypoints=insecure"
- "traefik.http.routers.whoami.rule=Host(`whoami-tf.thebidouilleur.xyz`)"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"

networks:
default:
external:
name: traefik_net

Nous avons alors créé la règle "whoami-tf.thebidouilleur.xyz" vers notre conteneur. On remarque que nous n'avons pas exposé de port, Traefik va passer par le réseau interne traefik_net pour accéder au service. C'est une couche de sécurité à ne pas négliger, vos services seront accessibles entre eux, et via le reverse-proxy.

Gestion des certificats https

Maintenant, si vous passez par internet pour accéder à vos services.. C'est peut-être pratique d'avoir du https, et justement : Traefik gèrera vos certificats de manière automatique. Traefik utilise l'api gratuite de LetsEncrypt pour obtenir ses certificats, nous devons donc créer une entrée dédiée au https sur le port 443

On va donc mettre à jour notre configuration comme ceci :

entryPoints:
insecure:
address: ":80"
http:
redirections:
entryPoint:
to: secure
secure:
address: ":443"

certificatesResolvers:
letsencrypt:
acme:
email: "contact@thoughtless.eu"
storage: "/etc/traefik/acme.json"
# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
keyType: "EC256"
httpChallenge:
entryPoint: "insecure"

Redémarrez Traefik, et celui-ci tentera de générer les certificats pour les domaines configurés ! En accédant à la page suivante : http://traefik:8080 ,vous aurez un dashboard sur lequel vous verrez les routeurs "domaines d'entrés", les "services" (redirections), et si ce symbole apparait : Traefik a bien appliqué un certificat à ce router.

Il est également possible, avec un peu plus de configuration, d'obtenir un certificat Wildcard (certificat valide pour un domaine entier) avec Traefik. Pour le moment : je n'ai pas besoin d'un wildcard pour mes domaines. Si le sujet vous intéresse, voici un lien pour approfondir ça : Certificat Wildcard Traefik

Et si on allait plus loin ?

Traefik et Swarm

Depuis maintenant un peu plus d'un an, mes conteneurs tournent sur un cluster swarm (Si vous ne savez pas ce qu'est un Swarm, je vous renvoie vers cet article), et ça peut complexifier les choses lorsque les labels (permettant à traefik de comprendre quel docker correspond à quel domaine) fonctionnent un peu différemment.

Les labels classiquent ne fonctionnent que sur la machine hote (par exemple: Worker01) mais si Traefik est sur la machine Worker02, les labels des conteneurs ne seront pas visibles. Pour palier à ce problème, nous devons utiliser les mêmes labels … dans la section deploy d'un docker-compose.

Voici le docker-compose whoami adapté pour Swarm:

version: "3.7"

services:
whoami:
image: "containous/whoami"
container_name: "whoami"
hostname: "whoami"
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.entrypoints=insecure"
- "traefik.http.routers.whoami.rule=Host(`whoami-tf.thebidouilleur.xyz`)"
- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"

networks:
default:
external:
name: traefik_net

danger

à noter que cette structure ne fonctionne qu'avec les docker-compose de version >3.7

Et pour que le conteneur traefik puisse lire ces labels.. Il doit être sûr un manager du swarm. Nous devons donc également mettre à jour notre docker-compose de Traefik :

version: "3.7"

services:
traefik:
image: "traefik:v2.5"
container_name: "traefik"
hostname: "traefik"
networks:
- traefik-net
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./config:/etc/traefik"
deploy:
placement:
constraints:
- node.role == manager
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=secure"
- "traefik.http.routers.traefik.rule=Host(`traefik.forky.ovh`)"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"


networks:
traefik-net:
external: true
driver: overlay
name: traefik-net

et nous pouvons le déployer dans le swarm avec la commande

docker stack deploy -c docker-compose.yml traefik

Autre spécificité du Swarm : Si le port d'écoute du service n'est pas 80, il faudra préciser à Traefik le port à utiliser. C'est ce qu'on peut voir sur le docker-compose ci-dessus avec traefik.http.services.traefik.loadbalancer.server.port, ça sera la dernière différence entre Traefik sur une machine standalone et un cluster.

Maintenant, comment faire si nous voulons créer une règle automatique avec une machine qui n'est pas dans notre swarm ?

Astuce pour machines isolées du cluster

Jusque-là, nous avons 2 providers : le provider Docker (pour le cluster) et le provider file qui concerne les règles statiques (comme mon pfsense). Traefik n'accepte pas qu'on ait 2 providers du même type, ce qui veut dire que je ne peux pas surveiller le daemon docker de ma machine, ainsi que celui d'une machine distante.

Par exemple, mon Gitea est un conteneur qui n'est pas dans mon swarm, et comme c'est une machine que je redeploie régulièrement (et donc IP différente), j'aimerai beaucoup laisser traefik faire son travail, mais en le laissant en même temps s'occuper du swarm !

C'est là que j'ai découvert un projet Github répondant à ce besoin : Traefik-pop

Le schéma ASCII du dépôt parle de lui-même :

                        +---------------------+          +---------------------+
| | | |
+---------+ :443 | +---------+ | :3000| +------------+ |
| WAN |--------------->| traefik |--------------------->| gitea | |
+---------+ | +---------+ | | +------------+ |
| | | | |
| +---------+ | | +-------------+ |
| | redis |<---------------------| traefik-kop | |
| +---------+ | | +-------------+ |
| swarm | | gitea |
+---------------------+ +---------------------+

J'ai un peu modifié le dessin pour qu'il colle à mon exemple.

Si le dessin est un peu compliqué : Nous allons créer une base de donnée Redis (C'est plus facile pour moi de le mettre sur le swamr, mais théoriquement, vous pouvez la mettre où vous voulez). Cette bdd, sera utilisée en tant que provider Traefik pour mettre à jour les règles automatiquements ! Le docker-compose de mon gitea devient donc :

version: "3"
networks:
gitea:
external: false
services:
server:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
- GIT_DISCOVERY_ACROSS_FILESYSTEM=1
restart: always
networks:
- gitea
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "2200:22"
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitea.entrypoints=secure"
- "traefik.http.routers.gitea.rule=Host(`git.thoughtless.eu`)"
- "traefik.http.routers.gitea.tls.certresolver=letsencrypt"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"

En redémarrant Traefik, et en accédant au panel, on remarque un nouvel provider : Redis.

et en visualisant les règles : nous avons bien notre règle concernant Gitea !

Conclusion

Traefik est un des meilleurs reverses-proxy pour les infrastructures grandissantes. Celui-ci s'adapte à de nombreux besoins en proposant une couche d'automatisation sans négliger la gestion statique et manuelle. Celui-ci demande un temps d'adaptation qui sera vite rentabilisé.

J'espère que ce reverse-proxy vous inspirera pour une infrastructure scalable simple et fiable.

Merci de m'avoir lu !

· 8 min read
TheBidouilleur

[ Cet article provient de mon ancien-blog, celui-ci sera également disponible dans la partie "Documentation" du site ]

Introduction

Depuis que jai commencé l'informatique (depuis un peu moins d'une dizaine d'année), je ne me suis jamais préoccupé de comment je visualisais mes logs. Un petit view par ci, un gros grep par là.. mais aucune gestion avancée.

J'ai basé ma supervision sur Zabbix et Grafana qui m'affichent les metriques de chaque machine virtuelle individuellement. Et même si c'est bien pratique, je n'ai presque aucun visuel sur l'état de mes applications ! J'ai donc décidé de me renseigner sur Graylog et Elastic Search proposant une stack assez fiable et facile à mettre en place. Puis en voyant les ressources demandées, j'ai remis ce besoin à "plus tard", et j'ai remis "plus tard" à l'année prochaine.. Et ainsi de suite !

2 ans plus tard…

Aujourd'hui (Decembre 2021), une grosse faille 0day est dévoilée concernant Log4J, et on ne parle pas d'une "petite" faille, c'est une bonne grosse RCE comme on les aime !

Je ne suis pas concerné par Log4J, ce n'est pas utilisé dans Jenkins, et je n'ai aucune autre application basée sur Java ouverte sur internet. Mais j'aurai bien aimé savoir si mon serveur a été scanné par les mêmes IP que l'on retrouve sur les listes à bannir. Et c'est avec cet évenement que j'ai décidé de me renseigner sur "Comment centraliser et visualiser ses logs?".

Le choix de la stack

une stack est un groupement de logiciel permettant de répondre à une fonction. Un exemple classique est celui de la stack "G.I.T." (et non pas comme l'outil de versioning!) :

  • Grafana
  • Influxdb
  • Telegraf

C'est une stack qui permet de visualiser les mectriques de différentes machines, InfluxDB est la base de donnée stockant les informations, Telegraf est l'agent qui permet aux machines d'envoyer les métriques, et Grafana est le service web permettant de les visualiser.

Comme dit dans l'introduction, j'utilise Zabbix qui me permet de monitorer et collecter les metriques, et j'y ai couplé Grafana pour les afficher avec beaucoup de paramètrages.

Dans la centralisation de logs (et la visualisation), on parle souvent de la stack suivant:

**ELK**:

  • ElasticSearch
  • Logstash
  • Kibana

Mais cette stack n'est pas à déployer dans n'importe quel environnement, il est efficace, mais très lourd.

Dans ma quête pour trouver une stack permettant la centralisation de logs, j'apprécierai utiliser des services que je dispose déjà.
Et voici le miracle à la mode de 2021 ! La stack GLP : Grafana, Loki, Promtail.

Stack GLP

Là où j'apprécie particulièrement cette stack, c'est qu'il est léger. Beaucoup plus léger que ELK qui, même si très efficace, demande beaucoup.

Extrait doc ELK

De même que Graylog2 + Elastic Search (une très bonne alternative) qui demande presque un serveur baremetal low-cost à lui seul. Extrait doc graylog

Alors que Grafana / Loki ne demanderont que 2Go pour fonctionner efficacement et sans contraintes. (Grand maximum, à mon échelle : j'utiliserai beaucoup moins que 2Go)

Installer notre stack

Je pars du principe que tout le monde sait installer un Grafana, c'est souvent vers ce service que les gens commencent l'auto-hebergement (en même temps, les graphiques de grafana sont super sexy !).

Mais si vous n'avez pas encore installé votre Grafana (dans ce cas, quittez la salle et revenez plus tard), voici un lien qui vous permettra de le faire assez rapidement

Par simplicité, je ne vais pas utiliser Docker dans cette installation.

Partie Loki

J'ai installé Loki sur un conteneur LXC en suivant le guide sur le site officiel ici. Je passe par systemd pour lancer l'executable, et je créé à l'avance un fichier avec le minimum syndical (qui est disponible sur le github de Grafana)

auth_enabled: false

server:
http_listen_port: 3100
grpc_listen_port: 9096

common:
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory

schema_config:
configs:
- from: 2020-10-24
store: boltdb-shipper
object_store: filesystem
schema: v11
index:
prefix: index_
period: 24h

Je n'ai pas pris la peine d'activer l'authentification en sachant que je suis dans un LAN avec uniquement mes machines virtuelles. Je considère pas que mon Loki comme un point sensible de mon infra.

Après seulement 2-3 minutes de configuration, notre Loki est déjà disponible !

On peut dès maintenant l'ajouter en tant que datasource sur notre Grafana : Configuration de Loki sur Grafana

Contexte
  • J'utilise localhost car la machine possédant le grafana héberge également le Loki.*
  • Il se peut que Grafana rale un peu car notre base de donnée Loki est vide.

Partie Promtail

Promtail est l'agent qui va nous permettre d'envoyer nos logs à Loki, j'ai écris un role Ansible assez simple me permettant d'installer notre agent sur de nombreuses machines en surveillant les logs provenant de Docker, varlog et syslog.

Voici ma template Jinja2 à propos de ma configuration :

server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml

clients:
{% if loki_url is defined %}
- url: {{ loki_url }}
{% endif %}


scrape_configs:


- job_name: authlog
static_configs:
- targets:
- localhost
labels:
{% if ansible_hostname is defined %}
host: {{ ansible_hostname }}
{% endif %}
job: authlog
__path__: /var/log/auth.log


- job_name: syslog
static_configs:
- targets:
- localhost
labels:
{% if ansible_hostname is defined %}
host: {{ ansible_hostname }}
{% endif %}
job: syslog
__path__: /var/log/syslog

- job_name: Containers
static_configs:
- targets:
- localhost
labels:
{% if ansible_hostname is defined %}
host: {{ ansible_hostname }}
{% endif %}
job: containerslogs
__path__: /var/lib/docker/containers/*/*-json.log

- job_name: DaemonLog
static_configs:
- targets:
- localhost
labels:
{% if ansible_hostname is defined %}
host: {{ ansible_hostname }}
{% endif %}
job: daemon
__path__: /var/log/daemon.log

Si vous n'êtes pas à l'aise avec des templates Jinja2, vous trouverez une version "pure" de la config ici

Vous pouvez bien evidemment adapter cette template à vos besoins. Mon idée première est d'avoir une "base" que je peux mettre sur chaque machine (en sachant aussi que si aucun log n'est disponible, comme pour Docker, Promtail ne causera pas une erreur en ne trouvant pas les fichiers)

Une fois Promtail configuré, on peut le démarrer : via l'executable directement :

/opt/promtail/promtail -config.file /opt/promtail/promtail-local-config.yaml

ou via systemd (automatique si vous passez par mon playbook) :
systemctl start promtail

Une fois cet agent un peu partout, on va directement aller s'amuser sur Grafana !

Faire des requetes à Loki depuis Grafana

On va faire quelque chose d'assez contre-intuitif : nous n'allons pas commencer par faire un Dashboard : on va d'abord tester nos requetes ! Scrollez pas, je vous jure que c'est la partie la plus fun !

Sur Grafana, nous avons un onglet "Explore". Celui-ci va nous donner accès à Loki en écrivant des requetes, celles-ci sont assez simple, et surtout en utilisant l'outil "click-o-drome" en dépliant le Log Browser Metric browser Pardon j'ai un chouïa avancé sans vous...

Avec la template que je vous ai donné, vous aurez 4 jobs :

  • daemon
  • authlog
  • syslog
  • containersjobs

Ces jobs permettent de trier les logs, on va tester ça ensemble. Nous allons donc selectionner la machine "Ansible", puis demander le job "authlog". Je commence par cliquer sur Ansible, puis Authlog. Grafana me proposera exactement si je souhaite choisir un fichier spécifique. Si on ne précise pas de fichier(filename) Grafana prendra tous les fichiers (donc aucune importance si nous n'avons qu'un seul fichier)

vous remarquerez plus tard que dès notre 1ere selection, grafana va cacher les jobs/hôte/fichier qui ne concernent pas notre début de requete.

Selections de paramètres

En validant notre requete (*bouton show logs*)

Visualisation des logs de la machine 'Ansible'

Nous avons donc le résultat de la requete vers Loki dans le lapse de temps configuré dans Grafana (1h pour moi). Mon authlog n'est pas très interessant, et mon syslog est pollué par beaucoup de message pas très pertinents.

Nous allons donc commencer à trier nos logs !

En cliquant sur le petit "?" au dessus de notre requete, nous avons une "cheatsheet" résumant les fonctions basiques de Loki. Nous découvrons comment faire une recherche exacte avec |=, comment ignorer les lignes avec != et comment utiliser une expression regulière avec |~

Je vous partage également une cheatsheet un peu plus complète que j'ai trouvé sur un blog : ici

Ainsi, on peut directement obtenir des logs un peu plus colorés qui nous permettrons de cibler l'essentiel !

Log de la machine 'Drone-Runner'

(L'idée est de cibler les logs sympas avec les couleurs qui vont avec)

Conclusion

Si on entend souvent parler de la suite ELK, ça n'est pas non-plus une raison pour s'en servir à tout prix ! Loki est une bonne alternative proposant des fonctionnalitées basiques qui suffiront pour la plupart.

danger

Ce projet est obsolète, il peut être risqué de s'en servir dans un environnement sensible.

· 6 min read
TheBidouilleur

[ This article is from my old-blog, it will also be available in the "Documentation" section of the site ]

Introduction

The world of containerization has brought many things into system administration, and has updated the concept of DevOps. But one of the main things that containers (and especially Docker) bring us is automation.

And although Docker is already complete with service deployment, we can go a little further by automating container management! And to answer that: Docker Inc. offers a tool suitable for automatic instance orchestration: Docker Swarm.

What is Docker Swarm?

As previously stated: Docker Swarm is an orchestration tool. With this tool, we can automatically manage our containers with rules favoring High-availability, and Scalability of your services. We can therefore imagine two scenarios that are entirely compatible:

  • Your site has a peak load and requires several containers: Docker Swarm manages replication and load balancing
  • A machine hosting your Dockers is down: Docker Swarm replicates your containers on other machines.

So we'll see how to configure that, and take a little look at the state of play of the features on offer.

Create Swarm Cluster

For testing, I will use PWD (Play With Docker) to avoid mounting this on my infra:)

So I have 4 machines under Alpine on which I will start a Swarm cluster. 4 terminals, one per node

The first step is to define a Manager, this will be the head of the cluster, as well as the access points to the different machines. In our case, we will make it very simple, the manager will be Node1.

To start the Swarm on the manager, simply use the 'docker swarm init' command. But, if your system has a network card count greater than 1 (Fairly easy on a server), you must give the listening IP. In my case, the LAN interface IP (where VMs communicate) is 192.168.0.8. So the command I'm going to run is

docker swarm init èèadvertise-addr 192.168.0.8

Docker says:

Swarm initialized: current node (cdbgbq3q4jp1e6espusj48qm3) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join —token SWMTKN-1-5od5zuquln0kgkxpjybvcd45pctp4cp0l12srhdqe178ly8s2m-046hmuczuim8oddmk08gjd1fp 192.168.0.8:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.`

In summary: The cluster is well started, and it gives us the exact command to join the cluster from other machines! Since Node1 is the manager, I just need to run the docker swarm join command on Node2-4.

docker swarm join --token SWMTKN-1-5od5zuquln0kgkxpjybvcd45pctp4cp0l12srhdqe178ly8s2m-046hmuczuim8oddmk08gjd1fp 192.168.0.8:2377

Once completed, you can view the result on the manager with the command 'docker node ls' All node in terminal

Deploy a simple service

If you are a docker run user and you refuse docker-compose, you should know one thing: i don't like you. As you are nice to me, here is a piece of information that won't help: the equivalent of 'docker run' in Swarm is 'docker service'. But we're not going to get into that in this article.

Instead, we will use the docker-composed equivalent, which is the docker stack. So first of all, here's the .yml file

version: "3"
services:
viz:
image: dockersamples/visualizer
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
ports:
- "8080:8080"
deploy:
replicas: 1
placement:
constraints:
- node.role == manager

Before you start it, you'll probably notice the deploy part that lets you give directions to Swarm. So we can add constraints to deploy this on the manager(s), ask the host to limit the use of resources, or manage replicas for load balancing.

This first container will be used to have a simple dashboard to see where the Dashboards are positioned, and avoid going to CLI only for this function.

We will deploy this compose with the following command:

docker stack deploy —compose-file docker-compose.yml swarm-visualize

Once the command is complete, you simply open the manager's web server at port 8080. WEBUI Swarm

So we now have a web panel to track container updates.

Simplified management of replicas

When you access a container, you must go through the manager. But there is nothing to prevent being redirected to the 3-4 node via the manager. This is why it is possible to distribute the load balancing with a system similar to HAProxy, i.e. by redirecting users to a different container each time a page is loaded.

Here is a docker-compose automatically creating replicas:

version: '3.3'
services:
hello-world:
container_name: web-test
ports:
- '80:8000'
image: crccheck/hello-world
deploy:
replicas: 4

And the result is surprising: Scaling hello-world with 4

We can also adjust the number of replica. By decreasing it:

docker service scale hello-world_hello-world=2

Scaling hello-world with 2

Or by increasing it:

docker service scale hello-world_hello-world=20

Scaling hello-world with 20

What about High Availability?

I focused this article on the functions of Swarm, and how to use them. And if I did not address this item first, it is because every container created in this post is managed in HA! For example, I will forcibly stop the 10th replica of the "Hello world" container, which is on Node1. And this one will be directly revived, Kill hello-world

Okay, But docker could already automatically restart containers in case of problem, how is swarm different?

And to answer that, I'm going to stop the node4 Kill Node4

It is noted that the other nodes distribute automatically (and without any intervention) the stopped containers. And since we only access services through managers, they will only redirect to the containers that are started. One of the servers can therefore catch fire, the service will always be redundant, balanced, and accessible.

Conclusion

Docker-Swarm is a gateway to application clusters that are incredibly complex without a suitable tool. Swarm is easy to meet special needs without any technical expertise. In a production environment, it is advisable to switch to Kubernetes or Nomad which are much more complete and powerful alternatives.

I encourage you to try this kind of technology that will govern our world of tomorrow!

Thanks for reading