Contenedores de Linux en un par de l铆neas de c贸digo

Como continuaci贸n del art铆culo anterior sobre KVM, publicamos una nueva traducci贸n y comprendemos c贸mo funcionan los contenedores usando el ejemplo de ejecuci贸n de una imagen de Docker de busybox.


Este art铆culo sobre contenedores es una continuaci贸n del art铆culo anterior sobre KVM. Me gustar铆a mostrarle exactamente c贸mo funcionan los contenedores ejecutando una imagen de Docker de busybox en nuestro propio contenedor peque帽o.



A diferencia de la m谩quina virtual, el contenedor es muy vago y vago. Lo que normalmente llamamos contenedor es un paquete de c贸digo independiente con todas las dependencias necesarias que pueden enviarse juntas y ejecutarse en un entorno aislado dentro del sistema operativo host. Si cree que esta es una descripci贸n de una m谩quina virtual, profundicemos en el tema y veamos c贸mo se implementan los contenedores.



BusyBox Docker



Nuestro principal objetivo ser谩 ejecutar una imagen de busybox normal para Docker, pero sin Docker. Docker usa btrfs como sistema de archivos para sus im谩genes. Intentemos descargar la imagen y descomprimirla en un directorio:



mkdir rootfs
docker export $(docker create busybox) | tar -C rootfs -xvf -


Ahora tenemos el sistema de archivos de imagen de busybox descomprimido en la carpeta rootfs . Por supuesto, puede ejecutar ./rootfs/bin/sh y obtener un shell que funcione, pero si miramos la lista de procesos, archivos o interfaces de red, podemos ver que tenemos acceso a todo el sistema operativo.



Intentemos crear un entorno aislado.



Clon



Como queremos controlar a qu茅 tiene acceso el proceso hijo, usaremos clone (2) en lugar de fork (2) . Clonar hace casi lo mismo, pero permite que se pasen banderas, lo que indica qu茅 recursos desea compartir (con el host).



Se permiten las siguientes banderas:



  • CLONE_NEWNET : dispositivos de red aislados
  • CLONE_NEWUTS : host y nombre de dominio (sistema de tiempo compartido UNIX)
  • CLONE_NEWIPC - Objetos IPC
  • CLONE_NEWPID - identificadores de proceso (PID)
  • CLONE_NEWNS - puntos de montaje (sistemas de archivos)
  • CLONE_NEWUSER : usuarios y grupos.


En nuestro experimento, intentaremos aislar procesos, IPC, redes y sistemas de archivos. As铆 que comencemos:



static char child_stack[1024 * 1024];

int child_main(void *arg) {
  printf("Hello from child! PID=%d\n", getpid());
  return 0;
}

int main(int argc, char *argv[]) {
  int flags =
      CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET;
  int pid = clone(child_main, child_stack + sizeof(child_stack),
                  flags | SIGCHLD, argv + 1);
  if (pid < 0) {
    fprintf(stderr, "clone failed: %d\n", errno);
    return 1;
  }
  waitpid(pid, NULL, 0);
  return 0;
}


El c贸digo debe ejecutarse con privilegios de superusuario; de lo contrario, la clonaci贸n fallar谩.



El experimento da un resultado interesante: el PID hijo es 1. Sabemos muy bien que el proceso de inicio suele tener PID 1. Pero en este caso, el proceso hijo obtiene su propia lista de procesos aislados, donde se convirti贸 en el primer proceso.



C谩scara de trabajo



Para facilitar el aprendizaje de un nuevo entorno, comencemos un shell en el proceso hijo. Ejecutemos comandos arbitrarios como Docker Run :



int child_main(void *arg) {
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Ahora, al iniciar nuestra aplicaci贸n con el argumento / bin / sh, se abre un shell real en el que podemos ingresar comandos. Este resultado demuestra lo equivocados que est谩bamos cuando hablamos de aislamiento:



# echo $$
1
# ps
  PID TTY          TIME CMD
 5998 pts/31   00:00:00 sudo
 5999 pts/31   00:00:00 main
 6001 pts/31   00:00:00 sh
 6004 pts/31   00:00:00 ps


Como podemos ver, el proceso de shell en s铆 tiene un PID de 1, pero, de hecho, puede ver y acceder a todos los dem谩s procesos del sistema operativo principal. La raz贸n es que la lista de procesos se lee de procfs , que a煤n se hereda.



Entonces, desmonte procfs :



umount2("/proc", MNT_DETACH);




Ahora los comandos ps , mount y otros se rompen al iniciar el shell porque procfs no est谩 montado. Sin embargo, esto sigue siendo mejor que la filtraci贸n de procfs padre.



Chroot



Por lo general, se usa chroot para crear el directorio ra铆z , pero usaremos la alternativa pivot_root . Esta llamada al sistema mueve la ra铆z del sistema actual a un subdirectorio y asigna un directorio diferente a la ra铆z:



int child_main(void *arg) {
  /* Unmount procfs */
  umount2("/proc", MNT_DETACH);
  /* Pivot root */
  mount("./rootfs", "./rootfs", "bind", MS_BIND | MS_REC, "");
  mkdir("./rootfs/oldrootfs", 0755);
  syscall(SYS_pivot_root, "./rootfs", "./rootfs/oldrootfs");
  chdir("/");
  umount2("/oldrootfs", MNT_DETACH);
  rmdir("/oldrootfs");
  /* Re-mount procfs */
  mount("proc", "/proc", "proc", 0, NULL);
  /* Run the process */
  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Tiene sentido montar tmpfs en / tmp , sysfs en / sys y crear un sistema de archivos / dev v谩lido , pero omitir茅 este paso por brevedad.



Ahora solo vemos archivos de la imagen de busybox, como si estuvi茅ramos usando un chroot :



/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var

/ # mount
/dev/sda2 on / type ext4 (rw,relatime,data=ordered)
proc on /proc type proc (rw,relatime)

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    4 root      0:00 ps

/ # ps ax
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps ax


Por el momento, el contenedor parece bastante aislado, quiz谩s incluso demasiado. No podemos hacer ping a nada y la red no parece funcionar en absoluto.



Red



隆La creaci贸n de un nuevo espacio de nombres de red fue solo el comienzo! Debe asignarle interfaces de red y configurarlas para reenviar paquetes correctamente.



Si no tiene una interfaz br0, debe crearla manualmente (brctl es parte del paquete bridge-utils en Ubuntu):



brctl addbr br0
ip addr add dev br0 172.16.0.100/24
ip link set br0 up
sudo iptables -A FORWARD -i wlp3s0  -o br0 -j ACCEPT
sudo iptables -A FORWARD -o wlp3s0 -i br0 -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/16 -j MASQUERADE


En mi caso, wlp3s0 era la interfaz de red WiFi principal y 172.16.xx era la red del contenedor.



Nuestro lanzador de contenedores necesita crear un par de interfaces, veth0 y veth1, asociarlas con br0 y configurar el enrutamiento dentro del contenedor.



En la funci贸n main () , ejecutaremos estos comandos antes de clonar:



system("ip link add veth0 type veth peer name veth1");
system("ip link set veth0 up");
system("brctl addif br0 veth0");


Cuando finaliza la llamada a clone (), agregamos veth1 al nuevo espacio de nombres secundario:



char ip_link_set[4096];
snprintf(ip_link_set, sizeof(ip_link_set) - 1, "ip link set veth1 netns %d",
         pid);
system(ip_link_set);


Ahora, si ejecutamos ip link en un contenedor, veremos una interfaz loopback y alguna interfaz veth1 @ xxxx. Pero la red a煤n no funciona. Establezcamos un nombre de host 煤nico en el contenedor y configuremos rutas:



int child_main(void *arg) {

  ....

  sethostname("example", 7);
  system("ip link set veth1 up");

  char ip_addr_add[4096];
  snprintf(ip_addr_add, sizeof(ip_addr_add),
           "ip addr add 172.16.0.101/24 dev veth1");
  system(ip_addr_add);
  system("route add default gw 172.16.0.100 veth1");

  char **argv = (char **)arg;
  execvp(argv[0], argv);
  return 0;
}


Veamos c贸mo se ve:



/ # ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth1@if48: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue qlen 1000
    link/ether 72:0a:f0:91:d5:11 brd ff:ff:ff:ff:ff:ff

/ # hostname
example

/ # ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
64 bytes from 1.1.1.1: seq=0 ttl=57 time=27.161 ms
64 bytes from 1.1.1.1: seq=1 ttl=57 time=26.048 ms
64 bytes from 1.1.1.1: seq=2 ttl=57 time=26.980 ms
...


隆Trabajos!



Conclusi贸n



El c贸digo fuente completo est谩 disponible aqu铆 . Si encuentra un error o tiene una sugerencia, 隆deje un comentario!



隆Por supuesto, Docker puede hacer mucho m谩s! Pero es sorprendente la cantidad de API adecuadas que tiene el kernel de Linux y lo f谩cil que es usarlas para lograr la virtualizaci贸n a nivel del sistema operativo.



Espero que hayas disfrutado del art铆culo. Puede encontrar los proyectos del autor en Github y seguir Twitter para seguir las noticias y tambi茅n a trav茅s de rss .



All Articles