Blog de Amazon Web Services (AWS)
Picturesocial – Cómo contenerizar una aplicación en menos de 15 minutos
José Yapur, Senior Developer Advocate en AWS
Hablar de contenedores o containers en arquitectura de software es hablar de un hot topic. Algunos de nosotros hemos trabajado en esos conceptos por años mientras que otros recién están empezando. De todas formas, me gustaría ser quien te guíe en tu proceso de aprendizaje en containers. Juntos, en esta serie, construiremos Picturesocial, una nueva red social para compartir fotos y nacerá con una arquitectura basada en containers. Mientras la construimos tomaremos decisiones de arquitectura y exploraremos algunos retos y soluciones.
Pero, ¿Qué es un container?
Imagina la sala de tus sueños, con una pintura del color que te gusta, un sofá súper cómodo para leer o pasar el rato, una mesa de centro para poner la tasa de dos litros de café y ese libro de pinturas famosas de que nunca abriste pero se ve muy cool. Finalmente sientes que es el espacio perfecto, solo para darte cuenta que tienes que mudarte a otro lugar y comenzar todo el proceso desde cero.
Ahora imagina que estás diseñando exactamente la misma sala pero dentro de uno de esos containers de metal que llevan los barcos de carga. Esa sala ira contigo a donde sea que vayas, puede estar en un barco en el medio del mar, en un avión o en un camión cruzando el país.
Un container es exactamente eso, tu aplicación, runtime y sistema de archivos, todo empaquetado (como tu sala en un container de barco) para ejecutarse en cualquier lugar que soporte containers. A mi particularmente me gustan los container porque me dan la flexibilidad y libertad de correr mis aplicaciones donde quiera, cuando quiera y saber que funcionarán sin hacer cambios.
Cuando trabajas sobre aplicaciones contenerizadas lo más probable es que tus dependencias como base de datos o manejador de colas, no estén dentro de containers sino como servicios externos, al menos en los ambientes productivos, por eso es súper importante que tengas un lugar donde poner la información de configuración de tu aplicación como cadenas conexión, zonas horarias y otros que puedas necesitar; de esa forma si pasas de un ambiente a otro como de Desarrollo a QA y luego a Producción solo necesitas apuntar a la configuración correcta y evitas errores como el mío, donde desplegué una nueva versión de un sitio web completo de alta concurrencia y en producción con la configuración de desarrollo, apuntando a una base de datos local donde todos los clientes tenían el nombre de mis gatos.
Para iniciar vamos a aprender sobre algunos conceptos básicos de Docker container que nos ayudarán en nuestra ruta de aprendizaje.
- Image: Esta es una de las piezas fundamentales de un Container, es donde una aplicación y su estado viven. La imagen de un container contiene una aplicación empaquetada junto con sus dependencias, e información sobre qué procesos debe ejecutar. Puedes crear imágenes de container entregando un set de instrucciones especialmente formateadas y definidas, por ejemplo, en un Dockerfile. Siempre pienso en la Imagen como un archivo ISO[1], de esos que usaba para capturar el estado de mi sistema operativo con archivos, configuraciones y aplicaciones instaladas, y que podía ser utilizada en casi cualquier otra computadora. Comparado con un ISO, una imagen de container solo tiene una pequeña parte del sistema operativo, algunas librerías que yo le indique, el runtime y mi aplicación, por lo que termina siendo mucho más liviana y con menos requerimientos de computo al no tener que cargar un sistema operativo completo.
- Container: Cuando una Imagen se ejecuta pasa a llamarse un Container.
- Engine: Tu container necesita ejecutarse en algún lado donde Docker esté instalado. La forma que usa Docker para comunicarse con el Hardware donde está instalado es por medio de APIs, esas APIs son el Engine. Con el Engine, tu container obtiene acceso a los recursos de hardware como CPU, almacenamiento y redes.
- Registry: Es el lugar donde almacenas tus Imagenes de Container. El registry puede ser público o privado. Un registry no solo almacena la última (latest) imagen, sino también usa tags y metadata como 1/ cuándo fue subida tu imagen, 2/ quién sube la imagen y 3/ cuándo la imagen es usada. Es por eso que no podemos hablar de containers sin un registry, incluso cuando trabajamos localmente, nuestra computadora es el registry.
Ahora que tenemos un poco de contexto vamos a dar un vistazo al Dockerfile. Para mi, el Dockerfile es lo que solía hacer cuando estaba iniciando mi carrera como practicante, algunos de nosotros estuvimos a cargo de “Escribir el Manual de Instalación”, si, ese manual que nadie realmente leía y que cuando era necesario nunca servía y que, sin embargo, el principal problema de ellos siempre ha sido que fue escrito por humanos para humanos. En el caso de los Dockerfile, son un manual creado por humanos para máquinas, por lo que debe ser escrito de forma muy precisa y resuelve la mayoría de problemas de interpretación.
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env WORKDIR /app # Copy everything COPY . ./ # Restore as distinct layers RUN dotnet restore # Build and publish a release RUN dotnet publish -c Release -o out # Build runtime image FROM mcr.microsoft.com/dotnet/aspnet:6.0 WORKDIR /app COPY --from=build-env /app/out . EXPOSE 5111 ENV ASPNETCORE_URLS=http://+:5111 ENTRYPOINT ["dotnet", "HelloWorld.dll"]
Todos los Dockerfile necesitan un FROM y está usualmente ubicado en la primera línea. Es usado para especificar la imagen base que estás usando para crear tu propia imagen, por ejemplo, imagina que quieres crear una Imagen que necesita escribir un archivo en Ubuntu 20.04, entonces necesitarías un FROM similar a este:
FROM ubuntu:20.04
Del ejemplo de arriba podemos deducir cierta información básica sobre tagging. 1/ Ubuntu es el nombre de la imagen, tenemos disponibles millones de imágenes en Docker Hub, que en su mayoría son públicas y podemos usar para crear nuestras aplicaciones derivadas o usarlas tal cual. 2/ 20.04 es la versión de Ubuntu, luego de los dos puntos (“:”) siempre se indica el tag de la imagen, puedes usar tagging para especificar versiones de un app, ambientes, lenguajes, o lo que quieras. Si revisamos la pagina de la imagen de Ubuntu en Docker Hub podremos ver los diferentes tags disponibles y sus descripciones. https://hub.docker.com/_/ubuntu?tab=description
Algunos otros comandos importantes de Docker son:
RUN: usado para ejecutar múltiples comandos bash para preparar un container. Puedes usar RUN múltiples veces en tu Dockerfile. Por ejemplo el siguiente comando creará un directorio demo en el container.
RUN mkdir demo
CMD: usado para ejecutar comandos bash que solo pueden ser usados una vez, si tienes más de un CMD solo se ejecutará el último. CMD se utiliza para proveer valores por defecto a tu container. Por ejemplo, el siguiente comando creará un archivo llamado config en la carpeta .conf con el texto Hello World.
CMD ["printf", "Hello World", ">.conf/config"]
Si quieres conocer más sobre los comandos de Dockerfile, este link https://docs.docker.com/engine/reference/builder será de mucha ayuda. Además si quieres ver ejemplos de Dockerfile para varios lenguajes de programación y runtimes, puedes revisar este link https://docs.docker.com/samples/. Cuando creas tus propios Dockerfile es como cuando escribes un cuaderno de recetas, puedes reusarlo para aplicaciones similares cambiando pocas cosas, en mi caso usaré el mismo template de Dockerfile para todas las APIs .NET 6 que expondré en Picturesocial.
Ahora que tenemos un poco de contexto sobre containers, quiero contarles sobre Amazon Elastic Container Registry o Amazon ECR para los amigos. Puedes usar ECR para almacenar tus imágenes de container ya sea de forma pública o privada, la ventaja es que el acceso a tus imágenes es manejado por AWS Identity and Access Management (IAM) y es nativo de Docker, por lo que los comandos que ejecutas localmente con el Docker CLI son los mismos que ejecutarías para hacer operaciones ECR, como docker login o docker push
Pre-requisitos:
- Una cuenta de AWS, si no tienes puedes pedir una aquí: https://thinkwithwp.com/free/
- Si estás usando Microsoft Windows te sugiero trabajar usando WSL2: https://docs.microsoft.com/en-us/windows/wsl/install
- Si no tienes Git, desde acá puedes instalarlo: https://github.com/git-guides/install-git
- Si no tienes Docker instalado, acá las instrucciones https://docs.docker.com/engine/install/
- Acá los pasos para instalar AWS CLI https://docs.thinkwithwp.com/cli/latest/userguide/getting-started-install.html
- Si esta es tu primera vez trabajando con AWS CLI o necesitas un refresh de como setear tus credenciales en el terminal te sugiero seguir este paso a paso: https://thinkwithwp.com/es/getting-started/guides/setup-environment/. Si no quieres instalar todo desde cero, en este mismo link podrás seguir los pasos para configurar Amazon Cloud9 que es un entorno de desarrollo virtual, que incluye casi todo el toolset que necesitas para este paso a paso.
Paso a paso:
En esta oportunidad vamos a aprender sobre cómo contenerizar una API desarrollada en .NET 6 que retorna el texto pasado como parámetro en la URL, este template de API será usado para todas las API’s de Picturesocial. Vamos a contenerizar todo para mantener consistencia entre el ambiente de desarrollo local y los ambientes en nube.
He creado un repositorio de Github https://github.com/aws-samples/picture-social-sample con todo el código necesario para seguir este paso a paso, asegúrate de seleccionar la branch “ep1”.
- Primero, vamos a clonar nuestro repo base, así tendremos todos los archivos de esta API así como también el Dockerfile que usaremos para crear la imagen de container.
git clone https://github.com/aws-samples/picture-social-sample –branch ep1
- Una vez clonado, vayamos al directorio creado. Aseguremonos de siempre estar dentro de este directorio por el resto de esta guía, de esa forma todo se ejecutará sin problemas.
cd picture-social-sample/HelloWorld
- Ahora, antes de iniciar con los siguientes pasos, vamos a revisar si Docker está instalado correctamente y funcionando, probemos el siguiente comando y deberíamos recibir que tenemos al menos la versión 20.10.
docker –help
- Si abres el Dockerfile, que está dentro de nuestra carpeta, encontrarás exactamente la misma estructura que el que compartí en este artículo. No tengas miedo, cambia cosas y juega con el, la mejor forma de aprender siempre es experimentando por nuestra cuenta, equivocándonos y descubriendo cómo arreglarlo. Acá te doy algunas sugerencias de qué cosas probar.
- Añade una línea que imprima Hello World en el proceso de build de nuestra imagen de container.
RUN echo “Hello World” - Cambia el WORKDIR de app a api, asegúrate de cambiar todas las referencias a app en las lineas 2, 11 y 12 del Dockerfile.
- Añade una línea que imprima Hello World en el proceso de build de nuestra imagen de container.
- Ahora vamos a construir la imagen de container. Usaremos el comando docker build que será el responsable de crear la imagen, para ello usamos el parámetro -t para especificar el nombre de la imagen, tal como aprendimos antes, la estructura del nombre es nombreDeImagen:nombreDeTag si no especificamos un nombreDeTag se creará uno por defecto con el nombre latest. Finalmente especificamos el path donde nuestro Dockerfile está localizado, si seguimos en la carpeta HelloWorld, entonces nuestro path será un punto (.) debido a que se encuentra en el mismo directorio.
docker build -t helloworld:latest .
- Algo que aprendí mientras escribía este artículo es que si estás usando una Apple MacBook con Apple Silicon, el comando cambia un poco, de esta forma indicamos que construiremos la imagen para ejecutarse en linux/amd64.
docker buildx build —platform=linux/amd64 -t helloworld:latest .
- Vamos a ejecutar esta imagen y convertirla en un Container, para ello usaremos el comando docker run, por medio del parametro -d diremos que este container puede ejecutarse en segundo plano y con el parametro -p le diremos el puerto del container y el puerto que expondremos. Tal como podemos ver en nuestro Dockerfile, el container está usando el puerto 51111 y estamos mapeando el mismo puerto en el siguiente comando:
docker run -d -p 5111:5111 helloworld:latest
- Después de ejecutar este comando podremos abrir el navegador y escribir http://localhost:5111/api/HelloWorld/johndoe y si todo salió bien deberíamos recibir de respuesta un “Hello johndoe”, puedes cambiar el valor de la url a cualquier valor y probar que el API contenerizada devuelva el mensaje. Ahora que estamos obteniendo los valores esperados vamos a seguir con los pasos necesarios para subir la imagen a nuestro container registry privado. Para eso ejecutamos el siguiente comando de AWS CLI, donde creamos el respositorio y por medio del parametro —repository-name indicamos el nombre “helloworld”
aws ecr create-repository --repository-name helloworld
- Ahora descubramos cual es el nombre de dominio calificado (FQDN) de nuestro registry, de forma que en los siguientes pasos podamos renombrar nuestras imagenes con el nombre completo, esa es la forma que usa Docker para identificar imagenes locales o remotas. Para este proyecto usaremos la región us-east-1 y asumiremos que nuestro AWS Account Id es 111122223333 por lo que el nombre debería quedar así:
[aws account id].dkr.ecr.[aws region].amazonaws.com #for example for account id: 111122223333 on region: us-east-1 111122223333.dkr.ecr.us-east-1.amazonaws.com
- Ya tenemos el nombre de nuestro registry, ahora vamos a iniciar sesión en el CLI de Docker, recuerda reemplazar el AWS Account Id y la región, tal como lo hicimos en el paso anterior. Cuando iniciamos sesión en Docker le estamos dando acceso a nuestro ambiente local para hacer push de imágenes de containers a un repositorio remoto como ECR.
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [aws account id].dkr.ecr.[aws region].amazonaws.com
- Ahora ya estamos listos para hacer push de nuestra imagen, pero primero tenemos que cambiarle el nombre de forma que incluya el FQDN, caso contrario no la podremos subir a nuestro repositorio remoto, esto sucede porque Docker no reconoce a donde pertenece sin que se lo digas explícitamente. Al ejecutar este comando obtendrás un id de digest, eso significa que hiciste bien.
docker tag helloworld:latest [aws account id].dkr.ecr.[aws region].amazonaws.com/helloworld:latest
docker push [aws account id].dkr.ecr.[aws region].amazonaws.com/helloworld:latest
Si seguiste los pasos hasta acá, significa que ¡Lo lograste! Felicitaciones, contenerizaste tu primera aplicación. Te sugiero que veas el video que hemos preparado de este post con un resumen de este artículo y la ejecución del paso a paso: https://bit.ly/3LjsxM3
El siguiente post estará enfocado en aprender acerca de Kubernetes, intentaré responder a la pregunta: ¿Qué es Kubernetes y por qué debería importarme?
Sobre el autor
José Yapur es Senior Developer Advocate en AWS con experiencia en Arquitectura de Software y pasión por el desarrollo especialmente en .NET y PHP. Trabajó como Arquitecto de Soluciones por varios años, ayudando a empresas y personas en LATAM.