Esta pagina se ve mejor con JavaScript habilitado

Introducción a Docker

 ·  🎃 kr0m

Docker es un sistema de contenedores el cual nos permite aislar aplicaciones, este último detalle es muy importante ya que lo distingue de otros sistemas de contenedores como podría ser OpenVZ, el concepto clave en Docker es que se lanzan aplicaciones y no contenedores.

El uso de contenedores supone una gran ventaja respeto a la virtualización completa como podría ser KVM, mediante contenedores obtendremos una densidad de máquinas mayor en cada nodo de computo pero por otra parte estaremos limitados a ejecutar SOs basados en Linux ya que compartirán la misma imagen de kernel.

Otro aspecto interesante y que diferencia Docker de otros sistemas basados en contenedores es que si es apropiado las librerias básicas del SO son compartidas también, suponiendo un ahorro de espacio significativo:

El concepto clave en Docker es que se lanzan aplicaciones y no contenedores, es decir se arranca un contenedor en el que se lanza la app, en un sistema de contenedores tradicional se lanza el contenedor y dentro de este la aplicación.

El tener un contenedor arrancado solo es la consecuencia de lanzar la app, cuando esta termina su ejecución, el contenedor ya NO es necesario y se apaga de forma automatica.

Docker se apoya en varias tecnologias existentes:

  • NameSpaces: Aislamiento entre contenedores
  • Cgroups: Control de recursos 
  • UnionFS: Los cambios se aplican en forma de capas, es decir, parte de una imagen base en RO y va añadiendo cambios, cuando se commitea una imagen esta pasa a ser la capa base en RO mode.
    •   El sistema de ficheros UnionFS puede ser: AUFS, btrfs, vfs, and DeviceMapper

Este artículo se compone de los siguientes apartados:


INSTALACIÓN

Lo primero será compilar el kernel de nuestro nodo de computo con soporte para CGROUPS, si nos dejasemos alguna opción por habilitar al compilar el ebuild de Docker nos aparecería un warning indicándonos la opción a habilitar dentro del kernel.

Compilamos Docker:

vi /etc/portage/package.use/docker

app-emulation/docker -aufs btrfs contrib device-mapper doc lxc overlay vim-syntax zsh-completion
emerge -av app-emulation/docker

Arrancamos el servicio y lo metemos en el arranque:

/etc/init.d/docker start
rc-update add docker default

NOTA: Si queremos que los contenedores tengan acceso al exterior sin natear debemos habilitar el forwarding:

vi /etc/sysctl.conf

net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1

USO BÁSICO

Docker tiene un sistema de imágenes remoto llamado dockerhub, este tiene tanto imágenes certificadas como de terceros, para hacer algunos test iniciales con Docker puede resultarnos util recurrir a ellos pero como mas adelante se explicará nosotros vamos a utilizar nuestra propia imagen.

docker search gentoo

Corremos bash dentro del contenedor, -i: STDIN input, -t: pseudo-terminal

docker run -t -i jgkim/gentoo-stage3 /bin/bash
exit

Vemos que el contenedor se ha detenido en cuanto se ha salido de la shell, lo cual tiene sentido ya que Docker corre aplicaciones NO contenedores.

docker ps

CONTAINER ID        IMAGE                        COMMAND                CREATED             STATUS              PORTS               NAMES

Dejamos una tarea en marcha dentro del contenedor:

docker run -d jgkim/gentoo-stage3 /bin/sh -c “while true; do echo hello world; sleep 1; done”
docker ps

CONTAINER ID        IMAGE                        COMMAND                CREATED             STATUS              PORTS               NAMES
8b68c1646f22        jgkim/gentoo-stage3   "/bin/sh -c 'while t   3 seconds ago            Up 1 seconds                            trusting_lovelace

Podemos ver la salida estandar de nuestro container:

docker logs 8b68c1646f22

hello world
hello world
hello world
hello world

Tembién podemos verlos al estilo tail -f:

docker logs -f 8b68c1646f22

Paramos el container:

docker stop 8b68c1646f22
docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Podemos entrar en el container y NO cerrar la sesión, estilo screen:

docker run -t -i jgkim/gentoo-stage3 /bin/bash
Crtl+PQ

Para volver a entrar:

docker ps

CONTAINER ID        IMAGE                 COMMAND             CREATED        STATUS              PORTS               NAMES
25d21650646e        jgkim/gentoo-stage3   "/bin/bash"         14 seconds ago     Up 12 seconds                           mad_pare            
docker attach --sig-proxy=false 25d21650646e

NOTA: Si se attachean dos sesiones el comportamiento es muy similar a una sesión de screen ;)

Esto esta muy bien pero NO podemos fiarnos de una image de Gentoo que no hayamos instalado nosotros mismos así que nos bajamos el stage3 e importamos en docker:

REPOSITORY          TAG                 IMAGE ID            CREATED        VIRTUAL SIZE
gentoo              latest              264d2b032543        6 minutes ago    773.1 MB

Actualizamos el sistema operativo e instalamos las herramientas básicas:

docker run -ti gentoo /bin/bash
emerge --sync
emerge eix htop vim
eix-sync
emerge -uDav world
emerge --depclean
python-updater
exit


COMMITS

Los cambios en un container de docker son muy parecidos a los cambios en un repo de git, cuando salgamos debemos commitear los cambios o los perderemos:

docker ps -l

CONTAINER ID        IMAGE                        COMMAND             CREATED             STATUS                     PORTS               NAMES
9a5a0b8c7f13        gentoo              "/bin/bash"         6 minutes ago          Exited (0) 2 seconds ago                       grave_colden        

Commiteamos los cambios para que a partir de ahora nos sirva como imagen base:

docker commit 9a5a0b8c7f13 gentoo/updated
docker images

REPOSITORY          TAG                 IMAGE ID            CREATED        VIRTUAL SIZE
gentoo/updated      latest              4eedf047deb9        47 seconds ago    1.175 GB
gentoo              latest              264d2b032543        14 minutes ago    773.1 MB

NOTA: Para evitar la corrupción de ficheros Docker pausa el contenedor antes de hacer el commit, si queremos evitarlo utilizaremos el parametro -p false:

docker commit -p false 9a5a0b8c7f13 gentoo/updated

En este momento podriamos empaquetar nuestro container y distribuirlo si así lo deseamos:

docker save -o gentoo-updated.tar 9a5a0b8c7f13

Para importar:

cat gentoo-updated.tar | docker import - ‘gentoo_imported’
docker images

REPOSITORY          TAG                 IMAGE ID            CREATED        VIRTUAL SIZE
gentoo_imported     latest              b1c4c04c1d2b        About a minute ago    1.345 GB
gentoo/updated      latest              4eedf047deb9        5 minutes ago    1.175 GB
gentoo              latest              264d2b032543        19 minutes ago    773.1 MB

NOTA: Hay una diferencia entre save y export, con save se puede hacer rollback a cualquier estado anterior del container mientras que con export solo se tiene el ultimo estado.

Empaquetar contenedores rara vez se hace ya que lo ideal es tener un servidor registry propio donde almacenar las imágenes.

Hagamos una prueba de rollback:

docker run -t -i gentoo/updated /bin/bash
rm -rf /etc/*

Ups, la hemos hecho buena…

No problem, como no hemos commiteado tan solo salimos perdiendo los cambios:

exit
docker run -t -i gentoo/updated /bin/bash
ls -la /etc/passwd

-rw-r--r-- 1 root root 651 Jul 23 04:40 /etc/passwd

Puede que cometamos algún error y ni tan siquiera lo sepamos, en tal caso la única solución es revertir a un commit en el que sepamos que todo funciona correctamente:

docker run -t -i gentoo/updated /bin/bash
rm -rf /etc/*
exit
docker ps -l

CONTAINER ID        IMAGE                  COMMAND             CREATED             STATUS                      PORTS               NAMES
2500d9c520ea        gentoo/updated      "/bin/bash"         About a minute ago       Exited (0) 3 seconds ago                       ecstatic_colden    
docker commit 2500d9c520ea gentoo/updated_sinpasswd
docker images
REPOSITORY                 TAG                 IMAGE ID            CREATED        VIRTUAL SIZE
gentoo/updated_sinpasswd   latest              c3b9f653bf61        5 seconds ago    1.175 GB
gentoo_imported            latest              b1c4c04c1d2b        5 minutes ago        1.345 GB
gentoo/updated             latest              4eedf047deb9        9 minutes ago        1.175 GB
gentoo                     latest              264d2b032543        23 minutes ago       773.1 MB
docker run -t -i gentoo/updated /bin/bash
ls -la /etc/passwd
-rw-r--r-- 1 root root 651 Jul 23 04:40 /etc/passwd
exit

Ahora imaginemos que hemos realizado varias configuraciones realmente laboriosas además del error, no vamos a deshechar todo nuestro trabajo porque se nos haya ido el dedo, los pasos a seguir serían:

  • Commitemos los cambios
  • Entramos en un commit anterior(puede ser mas comodo utilizando volúmenes)
  • Copiamos los ficheros borrados
  • Salimos
  • Entramos en el ultimo commit
  • Restauramos los ficheros borrados
  • Salimos
  • Commiteamos

Un comando bastante útil es docker diff que nos mostrará los cambios entre la última entrada en el contenedor y el último commit: 

docker run -t -i gentoo/updated /bin/bash
rm /etc/passwd
exit

La última entrada:

docker ps -l

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
f269a364a153        gentoo/updated      "/bin/bash"         22 seconds ago    Exited (0) 9 seconds ago                       evil_hawking      

Comparamos el estado de la última entrada(22 seconds ago) con el estado del último commit de la imagen: gentoo/updated

docker diff

C /root
C /root/.bash_history
C /etc
D /etc/passwd

Podemos observar que se han creado varios ficheros del history de bash y se ha eliminado /etc/passwd


NETWORKING

Docker utiliza iptables para redirigir tráfico del equipo anfitrión al contenedor, para hacer una prueba instalamos Apache:

docker run -t -i gentoo/updated /bin/bash
emerge apache
cd /etc/apache2/vhosts.d/
rm *
vi 00_kr0m.conf

Listen 80
NameVirtualHost *:80
<VirtualHost *:80>
 ServerAdmin kr0m@alfaexploit.com
 DocumentRoot /var/www/html
 ServerName *
 ErrorLog /var/log/apache2/docker_http.error_log
 CustomLog /var/log/apache2/docker_http.access_log combined
 DirectoryIndex index.php index.htm index.html
 <Directory "/var/www/html">
 Options -Indexes FollowSymLinks
 AllowOverride All
 Order allow,deny
 Allow from all
 </Directory>
</VirtualHost
mkdir /var/www/html
vi /var/www/html/index.html
<html><body><h1>DockerWeb by kr0m</h1></body></html>
vi /etc/apache2/httpd.conf
ServerName localhost
exit

Commiteamos los cambios:

docker ps -l

CONTAINER ID        IMAGE               COMMAND             CREATED        STATUS                     PORTS               NAMES
1049a59f7816        gentoo/updated      "/bin/bash"         3 hours ago        Exited (0) 5 seconds ago                       trusting_perlman    
docker commit 1049a59f7816 gentoo/apache

Arrancamos el contenedor pero esta vez indicando que el puerto WAN:7777 se redirigirá al LAN:80

docker run -d -p 7777:80 gentoo/apache /usr/sbin/apache2 -k start -D FOREGROUND
docker ps

CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                  NAMES
18d6cd9c9521        gentoo/apache       "/usr/sbin/apache2 -   7 seconds ago       Up 4 seconds        0.0.0.0:7777->80/tcp   dreamy_lalande      

Comprobamos que efectivamente la redirección funciona correctamente:

curl http://localhost:7777

<html><body><h1>DockerWeb by kr0m</h1></body></html>

NOTA: En algunos contenedores preinstalados con Apache vemos que se lanza un script directamente:

docker ps

CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS              PORTS                                                                         NAMES
169027bdabbe        fedora/apache              "/run-apache.sh"       17 seconds ago      Up 15 seconds       80/tcp                                                                        fedoreando         
docker run -i -t fedora/apache /bin/bash
cat run-apache.sh
#!/bin/bash
rm -rf /run/httpd/* /tmp/httpd*
exec /usr/sbin/httpd -D FOREGROUND

Podemos ver la redirección del puerto 7777 a la ip del contendor mediante iptables:

iptables -t nat -L -n

Chain DOCKER (2 references)
target     prot opt source               destination         
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:7777 to:172.17.0.18:80

La red interna creada por Docker se puede ver mediante ifconfig:

ifconfig

eth0: flags=67<UP,BROADCAST,RUNNING> mtu 1500
 inet 172.17.0.7 netmask 255.255.0.0 broadcast 0.0.0.0
 ether ae:76:9a:64:c5:05 txqueuelen 0 (Ethernet)

veth1e84f17: flags=67<UP,BROADCAST,RUNNING> mtu 1500
 inet6 fe80::ac76:9aff:fe64:c505 prefixlen 64 scopeid 0x20<link>
 ether ae:76:9a:64:c5:05 txqueuelen 1000 (Ethernet)

Podemos ver el bridge creado, todos los contenedores que esten en el mismo bridge serán accesibles entre ellos:

brctl show

bridge name    bridge id        STP enabled    interfaces
docker0        8000.ae769a64c505    no        veth1e84f17

NOTA: Como podemos ver el bridge no añade la interfaz eth0 si no veth1e84f17 cuya MAC es idéntica a la de eth0

Docker puede utilizar otro modo de interconexión entre contenedores llamado “linking system”, este se basa en el nombre de los containers para establecer los flujos de datos.

Este sistema tiene una gran ventaja y es que NO se expone ningún puerto al exterior, los datos pasan a traves del tunel creado por docker.

La conectividad se realiza mediante:

  • Variables de entorno
  • Modificando el /etc/hosts

Paramos y borramos el contenedor anterior:

docker stop 18d6cd9c9521
docker rm 18d6cd9c9521

Creamos el contenedor de base de datos:

docker run -t -i gentoo/updated /bin/bash
emerge dev-db/mysql
emerge --config =dev-db/mysql-5.6.24
vi /etc/mysql/my.cnf

bind-address = 0.0.0.0
/usr/bin/mysqld_safe &
mysql -uroot -p’XXXX'
use mysql
grant all privileges on *.* to root@'172.17.0.%' identified by 'XXXX';
flush privileges;
exit
exit
docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                      PORTS               NAMES
5be78a416521        gentoo/updated      "/bin/bash"         13 minutes ago      Exited (0) 2 seconds ago                       high_newton 

Commiteamos los cambios:

docker commit 5be78a416521 gentoo/mysql

Arrancamos el contenedor indicando el nombre MySQL:

docker run -d --name MySQL gentoo/mysql /usr/bin/mysqld_safe
docker ps

CONTAINER ID        IMAGE                COMMAND                CREATED             STATUS              PORTS                  NAMES
aae0431579ec        gentoo/mysql        "/usr/bin/mysqld_saf   6 seconds ago       Up 4 seconds                            MySQL               

Permitimos el tráfico entre contenedores:

iptables -I FORWARD 1 -s 172.17.0.0/24 -d 172.17.0.0/24 -j ACCEPT

Arrancamos el contenedor Apache linkandolo con MySQL:

docker run -t -i --name Apache --link MySQL:mydb gentoo/apache /bin/bash
cat /etc/hosts

172.17.0.18 mydb aae0431579ec MySQL
ping mydb
PING mydb (172.17.0.18) 56(84) bytes of data.
64 bytes from mydb (172.17.0.18): icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from mydb (172.17.0.18): icmp_seq=2 ttl=64 time=0.026 ms

NOTA: Las opciones de linkado son --link NOMBRE_CONTENEDOR:ALIAS --link MySQL:mydb

De este modo dentro del contenedor podremos referirnos a la base de datos como mydb 

Instalamos el cliente de mysql para hacer una conexión de prueba:

vi /etc/portage/package.use/mysql

dev-db/mysql -bindist -cluster -community -debug -embedded -extraengine -jemalloc -latin1 -max-idx-128 minimal -perl -profiling -selinux -ssl -static -static-libs -systemtap -tcmalloc -test
virtual/mysql -embedded minimal -static -static-libs
emerge -av dev-db/mysql
mysql -h mydb -uroot -p’XXXX'
Welcome to the MySQL monitor. Commands end with ; or g.
Your MySQL connection id is 1
Server version: 5.6.24-log Source distribution

NOTA: Si un contenedor es reiniciado se le asignará una nueva ip, en tal caso se actualizará de forma automática el fichero hosts en todos los contenedores enlazados con este.


VOLÚMENES

Los volúmenes son datos que pueden ser compartidos y reutilizados entre contenedores, un ejemplo básico podría ser el siguiente:

mkdir /webapp
echo alfaexploit.com > /webapp/asd
docker run -t -i --name Apache -v /webapp:/webapp gentoo/apache /bin/bash
cat /webapp/asd

alfaexploit.com

NOTA: De este modo varios servers pueden acceder a la misma información en real-time

Un buen truco es meter todos los logs en un mismo directorio con subdirectorios por contenedor:

-v /var/log/container00:/var/log

También permite montarlo como RO:

-v /src/webapp:/opt/webapp:ro

O incluso montar ficheros aislados:

-v ~/.bash_history:/.bash_history

Cabe la posibilidad de copiar ficheros de un contenedor sin tener que entrar dentro de el:

docker cp 169027bdabbe:run-apache.sh ./
cat run-apache.sh

Los volúmenes también pueden ser interesantes si queremos evitar el uso de tecnologias como NFS.


LIMPIEZA

Eliminar todos los contenedores de los que se haya salido:

docker ps -a | grep Exit | awk ‘{ print $1; }’ | xargs docker rm --

Eliminar los antiguos:

docker ps -a | grep ‘weeks ago’ | awk ‘{print $1; }’ | xargs docker rm --


SEGURIDAD

Algunas recomendaciones básicas de seguridad son:

  • Configurar las capabilities mínimas: Se permite el acceso a ciertos recursos del sistema
  • GRsec: Parches de seguridad a nivel de kernel
  • Auditd: Auditoria de syscalls
  • Docker-KVM: Solo se ejecuta el código guest una vez, todos los contenedores se ejecutan dentro de este KVM
  • AppArmor: Perfiles de acciones permitidas
  • Utilizar imagen base propia y servidor registry propio

TROUBLESHOOTING

  • https://github.com/wsargent/docker-cheat-sheet
  • dmesg
  • brctl show
  • iptables -L -n
  • tail -f /var/log/docker.log
  • docker top CONTAINERID
  • docker stats CONTAINERID
  • docker history IMAGE_NAME
  • docker stats $(docker ps -q)
  • Habilitar DEBUG:
    • vi /etc/conf.d/docker
    • DOCKER_OPTS="-log-level debug"
    • /etc/init.d/docker restart
Si te ha gustado el artículo puedes invitarme a un RedBull aquí