Aperçu

Formations préalables

N/A

Temps estimé

Objectifs

Préparation des participant.e.s

Installation de podman

sudo apt install podman

Survol du sujet

Ça mange quoi un container ?

Un container est une structure standardisée qui contient tout le code et dépendences nécessaires pour rouler un logiciel rapidement et de manière fiable, d'un environnement à un autre.

Le build flow est donc:

Containerfile --(build)-> image:tag --(push)-> registry

Et le run flow:

registry --(pull)-> image:tag --(run)-> container

Container vs VM

Le schéma suivant illustre les différences entre un container et une VM:

containers VS virtual machines

Le container est une abstraction au niveau de la couche applicative, utilisant le kernel de l'OS de la machine hôte et roulant comme process isolé dans un espace utilisateur. Les containers sont très légers (quelques MBs) et consomment beaucoup moins de ressources (CPU / RAM / HDD) que les VMs.

Les VMs sont une abstraction du matériel physique et transforment un serveur en plusieurs serveurs, où chaque VM roule son propre OS. De ce fait, les VMs sont bien plus lourdes, généralement plus lentes à démarrer, et consommatrices de ressources.

LA RÈGLE D'OR DU CONTAINER EST QU'ON NE VEUT ROULER QU'UN SEUL PROCESS PAR CONTAINER

Si on veut déployer une stack web, avec Apache + Mysql + Redis, alors on va avoir 3 containers, chacun roulant le process du logiciel précédemment mentionné. Cette règle n'est pas une limitation fonctionnelle (car on est capable de se connecter dans un container et de rouler des commandes) mais plus un choix de design sur l'utilisation des containers.

Podman vs Docker

Le mot container va immédiatement faire sonner le nom "Docker" chez la pluspart des informaticien.ne.s, car c'est le moteur de containerization le plus célébre. Cependant, l'arrivée de Podman dans le marché est loin d'être négligeable, et certainement bienvenue. Les deux outils sont open-source, et permette de gérer (builder, rouler) des containers.

Podman se veut un remplacement ultra-compatible avec docker, le site officiel annonce même qu'on devrait pouvoir faire alias docker=podman sans problème. La différence majeure est que Docker utilise un daemon en arrière-plan, alors que Podman non. Le daemon de docker a besoin d'accès root, donc tout container roulé par docker va avoir ces accès car chaque commande docker va en réalité déléguer la tâche au daemon. Podman vient adresser ce point en faisant en sorte que chaque commande podman interagit directement avec le process qui gère le container - ceci diminue la surface d'attaque en permettant aux containers de rouler dans l'espace usager normal (mais c'est aussi possible de rouler en root au besoin).

L'autre avantage de podman est l'utilisation de pods (initialement conceptualisé par Kubernates), qui permet d'aggréger plusieurs containers ensemble dans ce qu'on peut appeler une stack. L'équivalent docker le plus proche est l'outil docker-compose, mais qui nécessite un autre binaire ainsi que l'écriture de fichiers docker-compose.yaml. Il existe podman-compose qui est encore en développement, mais à date ce n'est pas encore fini.

The Basics

S'assurer d'avoir installé podman sur son local.

On va jouer un peu avec l'image nginx:

   1 # pull an image - EXPLIQUER POURQUOI LA 1ERE COMMANDE FAIL
   2 podman pull nginx
   3 podman pull docker.io/nginx
   4 
   5 # show images
   6 podman images
   7 
   8 # show containers - QUESTION: POURQUOI L'OUTPUT EST VIDE ?
   9 podman ps
  10 
  11 # rouler le container
  12 podman run nginx

Comment s'appelle le container ? Sur quel port écoute-t-il ? Parcourir man podman run pour pouvoir rouler le container nginx:

   1 # s'assurer que ça marche
   2 curl -I localhost:8080
   3 
   4 # voir les logs du container
   5 podman logs -f mynginx
   6 
   7 # se connecter dans le container
   8 podman exec -it mynginx bash

Où se trouvent les fichiers de config nginx ? Où se trouve le fichier html desservi par défault ? Un changement dans le fichier html est-il vu dans le host sous http://localhost:8080/ ?

   1 # voir les process qui roulent dans un container
   2 podman top mynginx
   3 
   4 # arrêter un container
   5 podman stop mynginx
   6 
   7 # supprimer un container
   8 podman rm mynginx
   9 
  10 # supprimer tous les containers non-utilisés
  11 podman rm $(podman ps -aq)

Bravo d'avoir roulé votre propre container nginx ! Cependant, on aimerait qu'il fasse autre chose que de simplement afficher la page de bienvenue, il faut donc qu'on lui donne des fichiers statiques à desservir ainsi que la possibilité de configurer nginx à travers des fichiers. Comment faire pour donner des fichiers à un container ? Deux options s'ouvrent à nous, et sont généralement documentées dans les README des containers, comme pour nginx - les deux sections suivantes vont les couvrir.

Problèmes

Privileged Ports

Si on essaye de rouler nginx sur le port 80 du host sans le user root, ce n'est pas possible:

$ podman run -p 80:80 nginx
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

Dans Linux, ça prend les permissions root pour binder à un port privilégié (i.e. en-deça de 1024). Une stratégie est donc de faire du port mapping, en assignant le port 80 du container nginx au port 8080 du host par exemple. Ceci a plusieurs intérêts:

https://www.yabage.me/images/post/11-host-docker-containers/docker-host-direct.png

Une situation courante est d'avoir un reverse-proxy sur le host (nginx a été spécialement conçu pour, mais Apache marche aussi) et qui va envoyer le traffic d'un certain domaine vers un certain container. Donc par exemple une config nginx:

   1 server {
   2   listen *:80;
   3 
   4   server_name           monsupersite.net;
   5 
   6   location / {
   7     proxy_pass            http://localhost:8080;
   8   # [...]
   9 

Ici, nginx (sur le host) est configuré pour écouter sur le port 80/443 du host. Et il va attraper tout traffic destiné à monsupersite.net (sur le host), puis l'acheminer à http://localhost:8080 qui est en fait un container (roulant lui même nginx / apache / python / on s'en fout). C'est au moment de rouler le container qu'on va dire "map le host port 8080 au container port 80" pour que le traffic se rende. C'est comme ça que marchent les serveurs [web] roulant des containers dans l'industrie :-)

N.B Attention lorsque l'on publie un port, tout le réseau du host y a accès. Pour de la R&D, ou bien avec un reverse-proxy en avant, c'est une bonne pratique de limiter l'accès au container à localhost seulement, donc:

podman run ... -p 127.0.0.1:8080:80 ...

Unqualified-search registries

Avant on pouvait simplement faire:  podman pull nginx  Mais comme on ne sait pas explicitement d'où vient l'image nginx (on assume que c'est myregistry.com), ça ouvre la porte à ce qu'une personne malveillante publie une image nginx sur myevilregistry.com et que l'image soit téléchargée de là. Donc maintenant il faut expliciter le registraire au moment du pull avec  podman pull docker.io/nginx . Il est aussi possible de configurer son client pour toujours checker docker.io: rajouter unqualified-search-registries=["docker.io"] dans son /etc/containers/registries.conf - mais généralement on veut faire ça avec des registraires privés (ex quay.io) où il faut s'authentifier.

Voir ce thread pour relire la même chose.

Mount points

On peut se connecter dans le container pour y rouler des commandes avec podman exec, mais on aimerait pouvoir échanger des fichiers. On aimerait donner des fichiers à servir au container, et récupérer les fichiers générés (ex logs / produit d'une computation etc). Pour se faire, on va utiliser des mount points.

Un mount point est un point de liaison entre un path sur le host et dans le container - les données sont mirroirées en temps réel. On peut configurer un ou plusieurs mount points pour un container. Lorsqu'on configure un mount point, si le path existait déjà dans le container alors tout son contenu va être écrasé par le contenu sur le host, donc pas moyen de "merger".

   1 # get nginx logs
   2 mkdir logs
   3 podman run -d --rm -p 127.0.0.1:8080:80 \
   4     --name mynginx \
   5     -v "$PWD"/logs:/var/log/nginx \
   6     nginx
   7 
   8 # check logs
   9 cat logs/access.log
  10 curl -I 127.0.0.1:8080
  11 cat logs/access.log

N.B. Attention de toujours spécifier un path complet avec "$PWD".

On crée maintenant un mount-point pour servir des fichiers HTML statiques

   1 # serve content
   2 mkdir html
   3 podman run -d --rm -p 127.0.0.1:8080:80 \
   4     --name mynginx \
   5     -v "$PWD"/html:/usr/share/nginx/html \
   6     nginx
   7 
   8 # what's in there? QUESTION: WHAT HAPPENED TO THE index.html FILE ?
   9 curl 127.0.0.1:8080
  10 
  11 # let's add our own
  12 echo "<html><body><h1>Dogs are the best!</h1><p>And cats are ... ok</p></body></html>" >> html/index.html
  13 curl 127.0.0.1:8080

C'est aussi possible de partager un fichier plutôt qu'un dossier, et de le configurer pour du read-only:

   1 # serve conf file
   2 vi conf/default.conf
   3 
   4 # put this content up here ^^^
   5 server {
   6     listen       80;
   7     listen  [::]:80;
   8     server_name  localhost;
   9     location / {
  10         root   /usr/share/nginx/html;
  11         index  index.html index.htm;
  12     }
  13 }
  14 
  15 podman run -d --rm -p 127.0.0.1:8080:80 \
  16     --name mynginx \
  17     -v "$PWD"/default.conf:/etc/nginx/conf.d/default.conf:ro \
  18     nginx

En tout et pour tout, ça donne un podman run qui a de l'allure !

   1 podman run \
   2     -d \
   3     --rm \
   4     -p 127.0.0.1:8080:80 \
   5     --name mynginx \
   6     -v "$PWD"/logs:/var/log/nginx \
   7     -v "$PWD"/html:/usr/share/nginx/html \
   8     -v "$PWD"/default.conf:/etc/nginx/conf.d/default.conf:ro \
   9     nginx

Révision ! Que fais chaque argument dans la commande précédente ?

Containerfile

Le Containerfile sert à définir comment builder une image. Précédemment on a téléchargé (pull) une image déjà buildée, donc on n'a pas eu besoin de s'en occuper. Il y a vraiment énormément d'images disponibles sur l'Internet, certaines étant très fiables (car "officielles") donc c'est safe de les utiliser (par exemple nginx, odoo, python etc). Parfois on a cependant besoin de créer nos propres images, soit car l'application n'a jamais été containerizé (comme le service web d'une compagnie) ou parce qu'on veut tweaker l'image à notre sauce.

C'est aussi la deuxième manière d'inclure des fichiers dans un container: directement au moment du build ! Quels sont les avantages / inconvénients à faire ça contrairement aux mount-points ?

Voici un example de Containerfile:

   1 FROM python:2.7-slim-buster
   2 
   3 # don't know which ones are necessary so installing one at a time from:
   4 # https://wiki.koumbit.net/MoinMoinConfiguration
   5 RUN apt-get update && \
   6     apt-get install -y curl vim nginx gunicorn python-ldap python-moinmoin python-openid && \
   7     mkdir /srv/koumbitwiki
   8 
   9 COPY wiki/ /srv/koumbitwiki
  10 COPY gunicorn-koumbitwiki.service /etc/systemd/system/
  11 COPY nginx/ /etc/nginx/
  12 
  13 # change wiki owner
  14 RUN chown -R www-data:www-data /srv/koumbitwiki
  15 
  16 EXPOSE 80
  17 EXPOSE 8080
  18 
  19 #USER www-data
  20 WORKDIR /srv/koumbitwiki
  21 COPY entrypoint.sh /tmp/entrypoint.sh
  22 
  23 ENTRYPOINT ["/tmp/entrypoint.sh"]

Il y a seulement quelques verbes prédéfinis qu'on utilise avec des arguments propres au projet.

Voir https://www.mankier.com/5/Containerfile pour plus d'info sur la syntaxe.

Voici un autre example, site/profile/files/goumbot/Containerfile:

   1 FROM docker.io/debian:buster-slim
   2 RUN apt-get update
   3 
   4 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install gozerbot
   5 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install ca-certificates
   6 RUN touch /var/log/gozerbot.log
   7 RUN chown gozerbot:gozerbot /var/log/gozerbot.log
   8 WORKDIR /var/lib/gozerbot
   9 USER gozerbot
  10 # It doesn't seem to work to redirect the output using /bin/sh,
  11 # so we will depend on the systemd unit to do it.
  12 #CMD ["/bin/sh", "-c", "gozerbot 2>&1 >> /var/log/gozerbot.log"]
  13 CMD gozerbot

Le processus de build peut prendre du temps, surtout pour des grosse usines à gaz (i.e. java). Chaque étape dans le build constitue un layer lorsqu'il est terminé, et ce dernier est caché. Donc si un Containerfile contient 10 étapes, un changement à la dernière instruction va invalider la dernière layer seulement. Le rebuild va être très rapide car 9/10 instructions seront cachées. Une stratégie d'optimization consiste à mettre les étapes les plus "stables" en premier afin de raccourcir les temps de build.

Contrairement à toutes les étapes du Containerfile qui ne prenne lieu qu'au moment du build, les instructions CMD et ENTRYPOINT se diffèrent en qu'elles ne sont pertinentes qu'au moment du run. Il y a une subtile différence entre les deux que je vous laisse rechercher en ligne.

Exercice: écriver un Containerfile pour nginx qui inclus directement les fichiers de config & HTML dans l'image. Rouler avec comme seul mount-point le dossier de log, et s'assurer que tout est beau.

Rootless

Le grand avantage de Podman est de pouvoir rouler des containers sans root ! Comment donc podman fait-il cela ? Cette vidéo youtube l'explique en grand détail, avec un résumé dans https://www.redhat.com/en/blog/understanding-root-inside-and-outside-container:

https://www.redhat.com/rhdc/managed-files/Table 1.png

Il faut commencer par faire la différence entre le user externe, qui roule la commande podman, et le user interne, qui roule le process (tel que défini par la clause USER dans le Containerfile). Parmis les 4 scénarios possibles (root/root, user/root, root/user, user/user), l'option root/user a été retenue par 42931 pour déployer Self-Service Password sur bento.koumbit.net. Ainsi, toutes les commandes podman sont roulées par root (donc on peut facilement faire podman ps pour voir tous les containers qui roulent), mais dans le container lui-même c'est un user non-privilégié qui roule, par exemple www-data. L'avantage est que si une personne malicieuse arrive à sortir du container, elle n'aura accès qu'à l'utilisateur www-data sur le host. C'est déjà un grand pas en avant comparé à docker qui roule tout sur root.

https://www.redhat.com/rhdc/managed-files/Table 2.png

Comme montré dans le tableau çi-haut, rouler la commande podman avec un user régulier aurait permis d'avoir une couche de sécurité supplémentaire. En effet, le user www-data qui roule la commande podman et le user www-data dans le container n'auraient pas été les mêmes, car le concept de User Namespaces génère un set de UID uniques et différents pour tous les users sur la machine host, et tous les users dans le container. Une personne malicieuse qui arrive à sortir du container n'aurait donc accès qu'à sweet fuck all dans ce scénario. Par contre, rouler les commandes podman en tant que non-root offre le désavantage qu'il faut configurer tout plein d'affaire pour faire marcher la patente. Je n'avais même pas réussi à faire podman pull il me semble (il y a même un article à ce propos).

N.B. Cerains containers s'attendent à rouler le process interne en root, comme apache. C'est un problème lorsqu'on veut binder sur le port 80/443 en roulant apache depuis un usager non-privilégié, tel que précédemment mentionné. Ça prend un petit tour de passe-passe pour y arriver:

   1 podman run \
   2     [...]
   3     --sysctl net.ipv4.ip_unprivileged_port_start=0 \ # permet à www-data de démarrer apache2 sur le port 80
   4     -p 127.0.0.1:8081:80 \ # only expose to localhost port 8081, mapped to port 80 in container
   5     -u 33 \ # uid for www-data, both on host and in container
   6 

Ici on a trouvé le uid de www-data sur le host (dans /etc/passwd) qu'on utilise avec -u, et l'option --sysctl net.ipv4.ip_unprivileged_port_start=0 permet à www-data de démarrer apache2 sur le port 80. Ça serait aussi possible de configurer apache pour ne pas bind sur le port 80 mais sur un port plus élevé, comme 8080. Plusieurs solutions au même problème :-)

Networking

Il y a tout un tas de documentation sur le networking docker / podman, je vous invite à en lire plus là-dessus, par exemple sur https://github.com/containers/podman/blob/main/docs/tutorials/basic_networking.md Cependant, c'est important de comprendre comment les containers communiquent avec le réseau.

Host

L'option host, la plus simple, est définie comme telle:

"the container is given access to a physical network interface on the host. This interface can configure multiple subinterfaces. And each subinterface is capable of having its own MAC and IP address. In the case of Podman containers, the container will present itself as if it is on the same network as the host."

https://github.com/containers/podman/raw/main/docs/tutorials/podman_macvlan.png

Tout est partagé entre le host et le container, et ce dernier est joignable depuis un client externe qui connait son adresse IP. L'avantage est une simplicité de configuration, il suffit de faire podman run --net=host [...] et pouf, le container est en avant-plan niveau réseau. Les désavantages sont multiples:

Bridge

L'option bridge, configurée par défaut, est définie comme telle:

"A bridge network is defined as an internal network is created where both the container and host are attached. Then this network is capable of allowing the containers to communicate outside the host."

https://github.com/containers/podman/raw/main/docs/tutorials/podman_bridge.png

Il y a donc un réseau en plus qui est créé juste pour les containers, dans lequel le host y figure aussi. Ce réseau peut, ou non, avoir accès à l'Internet. L'avantage de ce réseau est qu'on peut configurer ce réseau à sa guise, et que les ports sont disponibles pour s'y mapper. L'inconvénient est qu'il n'est pas aussi trivial que l'option host. D'après les commentaires #8 du billet 42931, pour que le container puisse communiquer avec l'extérieur ça prend au moins deux configurations (ref):

Dans puppet, il s'agirait d'indiquer comme dans site/profile/manifests/podman.pp:

   1   profile::sysctl::setting { 'net.ipv4.ip_forward':
   2     value => 1,
   3   }
   4   # Allow forwarding in firewall
   5   @nftables::rule { 'default_fwd-bride-containers-accept':
   6     content => 'accept',
   7   }

Dans l'idéal, on voudrait toujours utiliser ses containers en bridge networking plutôt qu'en host networking, à moins de vraiment savoir ce qu'on fait et d'avoir un besoin particulier.

Rétrospective

Pendant un maximum de 15 minutes, les participant.e.s sont invité.e.s à partager les éléments qui ont bien ou moins bien fonctionnés et les idées qui pourraient survenir pour des manières d'améliorer le processus.

Quelques éléments qui peuvent faire partie de la rétrospective, dépendant de la grosseur du projet ou de la formation:

  • Sommaire collectif (e.g. résumé rapide en termes que tout le collectif peut comprendre)
    • Pas trop utile pour les projets vraiment simples ou les formations.

  • Chronologie des événements marquant pour le projet
    • Surtout utile pour les projet qui se sont étalés sur plusieurs jours ou plus ou bien quand beaucoup de choses se sont produites simultanément, ce qui a rendu la compréhension des influences de chaque événement complexe.

  • Les bons coups -- qu'est-ce qui a bien fonctionné et qu'on veut tenter de reproduire
  • Les problèmes
    • échecs -- avec l'aide de la chronologie (si elle a été faite), tenter de situer les échecs dans un contexte selon ce qui était connu des participant.e.s aux moments qui ont mené à l'échec
    • problèmes techniques
    • manques de ressources
    • perturbations externes
  • Une liste d'actions à court et/ou à plus long terme pour améliorer les choses telles que soulignées pendant les points précédents
    • Transférer les actions dans redmine pour qu'elles puissent être suivies!

N'hésitez pas à partager les échecs à l'extérieur de l'équipe puisqu'on peut apprendre beaucoup de ceux-ci, mais surtout évitez de les formuler comme un blâme sur la/les personne(s) ayant échoué.

Une rétrospective est surtout utile quand on la partage: ça permet aux autres d'apprendre de nos erreurs et aussi de nos idées d'améliorations. On peut par exemple envoyer une forme écrite par email, ou bien sauvegardée comme page wiki.

Information complémentaire

Sources:

Prochaines notions


CategoryFormation CategoryPodman CategoryFormationPublique

FormationPodman (last edited 2023-06-28 12:08:49 by virgile)