Docker para el Abuelo (Parte 2).
(Imagenes, Contenedores y Registry)
Pre-requisito
Haber completado la primera parte del minicurso de Docker para principiantes, publicado en el siguiente enlace
Imagenes y Contenedores
Es fundamental entender la diferencia entre ambos conceptos:
Docker Image:
-
Una Imagen Docker es una combinación de SO/FileSystem/Paquetes + Parámetros (flags, comandos, paths etc), también puede pensarse como un solo paquete en el cual tenemos definido TODO absolutamente todo lo que necesitamos para ejecutar nuestra aplicación, por supuesto desde un punto de vista más técnico, una imagen docker no es un solo paquete pero por ahora vamos a pensarlo como una caja negra y a medida que vayamos avanzando veremos más en detalle su interior.
-
Otra característica de una imagen docker es que no posee estado y nunca sufre cambios, es decir que cuando realizamos un “build” de una imagen en particular, el resultado siempre será el mismo, esto no quita que podamos hacer “build” sobre una nueva versión de dicha imagen docker.
-
En mucha documentación online van a encontrar 3 términos asociados a Docker Download-Build-Run o Build-Ship-Run, esto es justamente las operaciones que se pueden realizar sobre una Imagen Docker, uno puede descargar una imagen docker de una registry pública o privada (lo explicaremos más adelante), también se puede ejecutar el contenido de una imagen docker y levantar el container resultante (Run). Por ejemplo en la primer parte de este curso realizamos la descarga de la imagen docker Hello-World de una registry pública y luego creamos un contenedor a partir de dicha imagen.
-
También es correcto ver a una Imagen Docker como el resultado de la ejecución de una serie de pasos (Build Process) estos pasos están definidos en algo llamado Dockerfile que veremos más adelante.
Docker Container:
-
Un Container Docker es el resultado de realizar la operación RUN a una Imagen Docker, si vienen del palo del desarrollo de software, podríamos pensar a una Imagen Docker como una Clase y a un Container Docker como un objeto (instancia de dicha clase), ya que primero definimos una clase (Docker image) y luego la instanciamos (Docker Container)
-
Podemos obtener muchos containers a partir de una sola imagen y solo nos costará el espacio en disco de una sola imagen, esa es la mejor parte y un beneficio indudable de usar Docker containers.
-
Los contenedores son inmutables, esto significa, que una vez definido la imagen docker, el contenedor resultante siempre se ejecutará con la misma version de código fuente y dependencias, por lo que no tendremos problemas típicos de cambios de versiones o de arquitectura de SO.
Una Imagen vale más que mil palabras, observemos la sig. imagen sacada del sitio oficial de Docker:
Registry (descargando y subiendo imagenes docker)
En la primer parte de este curso usamos para nuestro minilab una imagen de docker llamada hello-world, la descargamos de un lugar llamado Docker-Hub, Docker-Hub es parte del ecosistema de herramientas que ofrece Docker y también es una Docker Registry Una Docker Registry es un lugar donde podemos descargar imagenes de docker pero también podemos guardar nuestras propias imágenes y también compartirlas, una registry contiene muchos Docker Repository, y a su vez cada repositorio contiene Docker Images con el mismo nombre (es por esto que a veces van a encontrar en línea documentación que habla de un repo o una imagen como si fueran lo mismo) , por último, cada imagen docker tiene un atributo llamado tag (que contiene la versión de la imagen docker) esto nos sirve para versionar nuestras imágenes de docker, para hacerlo aún más sencillo, pensemos que una Docker Registry (DockerHub en nuestro ejemplo) es algo así como GitHub pero para imagenes Docker.
Una aproximación de la estructura de DockerHub y en general de cualquier registry publica o privada, sería algo así:
Explorando la Interfaz de DockerHub:
Vamos a analizar la interface de DockerHub ingresando al siguiente enlace
El enlace nos lleva a la imagen de docker hello-world (la misma que habíamos usado anteriormente, DockerHub nos dice que la imagen en cuestión se trata de una imagen oficial de Docker, esto significa que hay un equipo de Docker Inc. dedicado a mantener y publicar los cambios en la imagen, también de analizar y corregir vulnerabilidades etc. por su puesto, siempre recomiendo dos cosas, usar imágenes oficiales, o crear tus propias imágenes (mediante un Dockerfile). Otra cosa que nos muestra DockerHub además de permitirnos elegir el tipo de arquitectura de máquina (amd64, arn, ibm, windows etc) es el comando para descargar la imagen a nuestra máquina (docker pull hello-world) en la primer parte del curso no corrimos este comando, al menos no directamente, pero el pull lo ejecutó el Docker-CLI automáticamente al no encontrar dicha imagen de manera local en nuestra máquina. Usemos la barra de búsqueda de DockerHub para buscar la imagen oficial de docker de nginx por ejemplo, y veamos algunas otras cosas que nos muestra DockerHub:
El resultado de la búsqueda nos muestra una vista previa de la cantidad de descargas que tuvo la imagen, asi como también la reputación (cantidad de estrellas), además también nos avisa que se trata de una imagen oficial de docker, es decir, una imagen mucho más segura y confiable para utilizar, cuando hacemos clic en ella, vemos lo siguiente:
Además del comando pull, veamos la pestaña DESCRIPTION, se trata de una especie de README file de la imagen, pueden leer completa la info de la imagen, pero quizás lo mas importante de esta pestaña es la info de los tags que soporta la imagen de nginx, y algo mucho más interesante es que si hacemos clic en alguno de ellos, automáticamente nos lleva al código fuente del Dockerfile usado para construir la imagen:
NOTA: cuando se hace un pull de alguna imagen docker, el tag se indica inmediatamente después del nombre de la imagen usando dos puntos, de la siguiente manera docker pull nginx:1.15.12, si no se indica explícitamente el tag, docker asigna automaticamente el valor latest a la imagen, es un tag especial con el cual nos vamos a asegurar de descargar siempre la última versión de la imagen.
Busquemos otra imagen, por ejemplo la imagen oficial de docker para Python y veamos ahora que hay en la pestaña TAGS :
Vemos que tenemos información acerca de las vulnerabilidades existentes en la imagen, y si hacemos clic podemos ver aún mas en detalle de que se trata, si sos fanatico de la seguridad, tenes la info detallada de la imagen que decidas usar:
Consejo: siempre traten de usar imágenes oficiales, o construidas por ustedes mismos.
Cómo Luce una Imagen NO Oficial de Docker
Por supuesto existen tambien imagenes no oficiales, esto es, imagenes hechas por personas como vos y como yo, alguien que escribió un Dockerfile, creó una imagen y la subió a la registry pensando en compartirla (imagen pública) o simplemente para almacenarla y/o versionarla pero sin publicarla (repo privado) , en la imagen se puede ver una imagen generada con mi usuario de DockerHub, básicamente es una imagen creada con un dockerfile multistage para publicar una aplicación Angular con Nginx, la imagen tiene algunas modificaciones hechas para un cliente en producción es por esto que es privada, pero nos viene bien para el ejemplo, veamos:
La interfaz es similar a la de las imágenes publicas de docker, sin embargo vemos que tanto en el comando pull como en la descripción del nombre de la imagen docker, tenemos un slash antes del nombre de la imagen, lo que está antes del slash es el namespace del repositorio, que en caso que creen un usuario en DockerHub este será su nombre de usuario, es decir, por ej. docker pull manu123/ngprod:latest donde manu123 es el namespace del repo, y es también, mi nombre de usuario en DockerHub, en la siguiente sección veremos cómo subir nuestras imágenes a DockerHub.
INFO: cuando realizamos un pull de una imagen oficial de Docker, no es necesario especificar el NAMESPACE, ya que Docker-CLI tiene seteado el namespace por default. El namespace por default de DockerHub se llama library, por lo cual, si ejecutamos docker run library/hello-world obtendremos el mismo resultado que si omitimos dicho namespace.
En la imagen anterior, donde se encuentra el círculo rojo, si se tratara de un repositorio público, debería aparecer la siguiente info:
Con el registro gratuito de DockerHub tienen acceso únicamente a un repo privado, para acceder a más repos privados y/o imágenes privativas/licenciadas de Docker, deben optar por sus planes pagos.
NOTA: podemos explorar imagenes de Docker Pagas o Licenciadas en el siguiente enlace sin embargo, para usarlas debemos tener acceso a una cuenta de Docker Enterprise.
Hasta acá la parte teórica de DockerHub, ahora vamos a crear una imagen a partir de nuestro propio Dockerfile y a subirla a nuestra cuenta de DockerHub, cabe aclarar, que DockerHub no es la única Registry que existe, cualquiera puede crear su propia registry sea pública o privada, pero a la hora de manipularla, deberemos decirle a Docker-CLI donde se encuentra dicha Registry.
Build Process
Ahora que ya la tenemos clara acerca de cómo es el proceso de Download y Run en las imágenes y contenedores docker, vamos a hablar sobre el proceso Build, que básicamente es el proceso donde nace o se modifica una imagen docker.
Existen dos formas de “buildear” una imagen docker:
- Docker Commit: básicamente es tirar el comando y commitear dentro de un container corriendo, no lo veremos en este curso.
- Dockerfile: no es otra cosa que un archivo de texto, que contiene una serie de pasos para crear una imagen docker, es decir, una receta de Docker Images.
Vamos a ver el Build Process únicamente mediante Dockerfiles, la razón de esto es simple, lo que se usa en la vida real o al menos lo más usado es el Dockerfile, en mi caso, trabajo a diario en entornos dockerizados e incluso con k8s productivos, y conozco colegas que son Docker-Masters hace muchos años y hasta ahora no ví un solo Docker Commit, solo leí acerca de su uso, así que vamos a crear nuestro Dockerfile desde cero y veamos desde cero el Build Process de Docker Images mediante Dockerfiles.
Que es un Dockerfile ??
Es un archivo de texto, donde vamos a tener todas las directivas y parámetros necesarios para construir nuestra imagen docker, análogamente también podemos verlo como un archivo de dependencias en cualquier lenguaje de programación, por ej, en python el gestor de paquetes es pip, en nodeJS es npm, en cualquiera de los dos lenguajes si necesitaramos instalar 30 o 50 paquetes para hacer funcionar nuestra aplicación sería muy tedioso hacerlo de manera manual, por lo que python tiene el archivo requeriments.txt y node tiene el archivo package.json, ambos archivos cumplen la misma función, definir todas las dependencias necesarias para que la aplicación funcione; bueno, un Dockerfile es exactamente lo mismo pero lo que hacemos en el es definir todo lo necesario para que una imagen de docker nazca y funcione correctamente.
Creando nuestro primer Dockerfile o Dockerizando Flask:
Vamos a setear una aplicación básica con Flask y luego vamos a “Dockerizarla”, Flask es un microframework python, vamos a usarlo para nuestro ejemplo al ser muy facil de levantar y al mismo tiempo muy liviana, irónicamente DockerHub no tiene ningún repo oficial de Flask, pero eso no importa, vamos a crear nuestra propia imagen, a partir de una imagen oficial de python, y además vamos a publicarla en DockerHub.
ACLARACIÓN: existen muchas formas de dockerizar una aplicación Flask, no es el objetivo de este curso profundizar sobre optimización de Dockerfiles/construcción de imágenes, así que veremos una forma sencilla de hacerlo unicamente con propositos educativos, por lo que quizás falten cuestiones de seguridad y performance.
Creamos nuestra Flask App:
Creamos un directorio, y dentro, el archivo main de la app un requeriments.txt en el cual incluimos la última versión de Flask (la última versión al momento de escribir esta entrada), y el Dockerfile para el siguiente paso:
$ mkdir dockerflask
$ cd dockerflask
$ touch app.py requirements.txt Dockerfile
NOTA: no importa si no tenes conocimientos en python o flask, vamos a concentrarnos únicamente en el Dockerfile.
Contenido de app.py:
from flask import Flask
app= Flask(__name__)
@app.route('/')
def docker_flask():
return 'Ya sabemos como Dockerizar Flask'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Contenido de requirements.txt:
Flask==1.0.2
Ahora si, vamos por el Dockerfile
Vamos a copiar el siguiente contenido al Dockerfile y veamos que se indica en cada step:
FROM python:3.7.3-slim
MAINTAINER Manu Barrios "manuel.barrios@mymware.com"
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .
ENTRYPOINT [ "python" ]
CMD [ "app.py" ]
-
La instrucción FROM la usamos para definir en qué imagen ya existente de Docker nos vamos a basar para construir nuestra propia imagen, en este caso optamos por una imagen de python slim por ser muy ligera y totalmente funcional.
-
MANTAINER nos sirve para setear el autor del Dockerfile, este parámetro es muy útil sobre todo si estamos subiendo imágenes todo el tiempo a DockerHub o para contactarnos con los autores de alguna imagen si es que tenemos alguna sugerencia o problema de implementación etc.
-
Con WORKDIR seteamos el “Working Directory” o directorio de trabajo, todos los steps que sigan en el dockerfile, se ejecutarán en el directorio asignado con esta instrucción, WORKDIR se puede usar N veces en un dockerfile.
-
Con COPY copiamos directorios/archivos desde nuestro host, hacia la imagen/contenedor, en el ejemplo estamos copiando nuestro requeriments.txt dentro del WORKDIR seteado anteriormente (con el punto estamos seleccionando la totalidad del directorio).
-
La Instrucción RUN nos sirve para ejecutar comandos dentro de la imagen/contenedor, en el ejemplo estamos instalando las dependencias usando el gestor de paquetes pip.
-
ENTRYPOINT configura al container para que corra (run) como un ejecutable.
-
Con CMD le decimos a docker que ejecute el comando que pasamos como parametro cuando la imagen se cargue, en el ejemplo, vamos a ejecutar nuestro script app.py
Con esto último ya tenemos toda la estructura de directorios que necesitamos con el contenido correspondiente, si se fijan, acabamos de armar una aplicación Flask y ademas la vamos a levantar Dockerizada con solo 3 archivos:
Ahora que tenemos la receta (Dockerfile), vamos a realizar el Build Process sobre el mismo, para crear la imagen docker, tambien vamos a ponerle un nombre y un tag con:
$ docker build -t dockerflask:latest .
Vemos que por cada linea de código en nuestro dockerfile, el proceso de Build de la imagen nos muestra un step, en mi caso por ejemplo en el step 1, cuando se hace un pull de la imagen de python slim, Docker-CLI simplemente hace referencia al layer-ID donde se encuentra dicha imagen en mi host, esto se debe a que ya ocupé anteriormente dicha imagen, y Docker-CLI solamente tiene que referenciar el layer donde está almacenada, cada step tiene su layer-ID, a medida que vayan trabajando con Docker en general, van a notar lo útil que resulta la estructura del filesystem de docker en layers, ya que impide que tengan información redundante en sus máquinas y por lo tanto optimizan el uso de recursos.
Podemos comprobar fácilmente que nuestra imagen ya fue construida con el siguiente comando:
$ docker image ls
Ahora que ya tenemos nuestra imagen docker flask, la convertimos en un container con:
$ docker run -p 5000:5000 dockerflask:latest
Con el parametro -p estamos exponiendo puertos desde dentro del container y al mismo tiempo seteamos un forward desde y hacia nuestra máquina que está ejecutando el container, Docker-CLI nos avisará también el enlace donde se encuentra publicada nuestra aplicación flask (0.0.0.0:5000 o localhost:5000), ademas desde la consola veremos tambien los logs de nuestro container en funcionamiento (modo debug), también podemos indicarle a Docker-CLI que corra el contenedor en segundo plano si lo deseamos, con el parámetro -d.
Accedemos a nuestra app Flask Dockerizada en http://localhost:5000
Publicando nuestra Imagen en DockerHub
Pre-Requisito: para esta sección, es necesario que tengan su propio usuario DockerHub, pueden registrarse en el siguiente enlace
Nos logueamos a DockerHub desde nuestra consola:
$ docker login
Docker-CLI guardará nuestras credenciales encriptadas en el path indicado, esto es para evitarnos tener que estar ingresando todo el tiempo las credenciales, podemos verificar esta info tirando un cat al path:
Ya estamos logueados, ahora vamos a “taguear” nuestra imagen antes de tirar el push definitivo:
$ docker image tag dockerflask tu-usuario-dockerhub/dockerflask:latest
En otras palabras lo que estamos haciendo es asociar la imagen que acabamos de crear a un tag asociado a otro namespace (nuestra cuenta DockerHub), de hecho si exploramos las imágenes veremos que, la imagen “dockerflask:latest” tiene exactamente el mismo layer-ID que la imagen “tu-usuario-dockerhub/dockerflask:latest” por el comando que acabamos de ejecutar, veamos:
Ahora sí!!! PUSH
$ docker image push tu-usuario-dockerhub/dockerflask:latest
Como podemos ver en la imagen, hay layers que son subidas directamente y unos cuantos layers que son montados desde el namespace por default (library) debido a que se trata de layers de la imagen oficial de python.
Verificamos en nuestra cuenta de DockerHub y efectivamente, acabamos de subir nuestra imagen docker de Flask a DockerHub:
Muy buen trabajo!! qué Aprendimos?
- Aprendimos que es una Imagen Docker.
- Aprendimos que es un Container Docker.
- Sabemos la diferencia entre ambos.
- Aprendimos que es una Docker Registry.
- En particular aprendimos a utilizar DockerHub.
- Aprendimos más comandos de Docker-CLI (aprenderemos muchos más en la tercera parte del curso).
- Aprendimos a escribir nuestro primer Dockerfile y también aprendimos a “buildearlo” para convertirlo en una Imagen Docker personalizada.
- Dockerizamos Flask !!!
- Aprendimos como crear y actualizar nuestro propio repositorio publico en DockerHub.
Nos vemos en la última parte de este curso básico de Docker para principiantes: Docker en la Vida Real, con los siguientes temas:
- Data Volumes
- Docker Networks
- Docker-Compose
- Docker-Swarm
- BONUS: Lista de comandos Docker más usados en la vida real