Las ventajas que ofrece la plataforma pueden consultarse en el artículo Referencia: ventajas de aplicaciones hospedadas en esta plataforma.
Criterios base
-
No uses almacenamiento local para nada permanente. El almacenamiento local puede ser borrado en cualquier momento (cada nuevo deployment o si hay una falla en el servidor subyacente). Si requieres almacenamiento local que sobreviva nuevos deployments se puede asignar una unidad de red compartida a tu proyecto que aparecerá como un directorio (por ejemplo /persistent_data/) montado en tu contenedor.
- Esto es especialmente cierto para las sesiones de los usuarios si usan archivos normales. En este caso es preferible que se guarden en la base de datos o de no ser posible que se use el directorio persistente para almacenarlas.
- Esto es especialmente cierto para las sesiones de los usuarios si usan archivos normales. En este caso es preferible que se guarden en la base de datos o de no ser posible que se use el directorio persistente para almacenarlas.
-
La aplicación puede ser interrumpida y reiniciada en cualquier momento. Esto podría darse por cualquiera de los siguientes motivos:
- Al realizar un deployment nuevo, una vez que la nueva versión de la aplicación está estable, el contenedor de la versión anterior se detiene. Debido a que el tráfico ya se está redirigiendo al contenedor de la nueva versión el proceso será transparente al usuario.
- Cuando se realiza un reemplazo de los servidores subyacentes (para actualizar versiones del sistema operativo o hardware, por ejemplo) la infraestructura moverá las tareas del servidor a matar a uno nuevo. Esto es transparente para el usuario, pero ocasiona un redeployment.
- Si uno de los servidores subyacente falla a nivel hardware (caso raro pero que puede pasar ~1 vez al año) la infraestructura lo detectará y moverá las tareas a un nuevo servidor. Este proceso tardará algunos minutos.
- Al realizar un deployment nuevo, una vez que la nueva versión de la aplicación está estable, el contenedor de la versión anterior se detiene. Debido a que el tráfico ya se está redirigiendo al contenedor de la nueva versión el proceso será transparente al usuario.
-
Única imagen para todos los ambientes. La imagen de docker debe ser la misma para producción y staging (e idealmente para desarrollo local). La diferencia entre ambas debe ser en las valores en tiempo de ejecución dados por las variables de entorno pasados (el artículo Cómo configurar variables de entorno para mis contenedores explica cómo hacer esto). Esto permite una sola fuente de verdad. Hay que cuidar no guardar en la imagen cosas como un caché de un build específico (desarrollo, producción, etc). Estas operaciones se deben hacer cuando la imagen se cree en el bootstrap de la misma, no en la imagen docker.
-
Todos los logs deben ir a stdout y stderr. Esto es la práctica que se debe seguir siempre con docker: los logs de la aplicación no deben guardarse en un archivo sino siempre a la salida estándard y de error de la terminal. Esto permite que los logs se registren adecuadamente y puedan ser consultados con un ciclo de vida de duración bien planeado. De lo contrario se destruirían con el contenedor.
-
Migraciones de base de datos. Debes considerar cuándo aplicar las migraciones a la base de datos para una nueva versión de software.
- Idealmente deberías poder migrar la base de datos de un sistema corriendo sin que la versión anterior de código falle por los cambios en el esquema.
- Este proceso se debería realizar cuando el contenedor arranque pero antes de que la nueva aplicación lo haga.
- Debe haber un plan de rollback en caso de que la migración falle para evitar dejar la base de datos en un estado inconsistente.
-
Variables de entorno y secretos. En tu archivo de definición de la aplicación (
hosting/deploy_env/task-definition.json
) habrá una sección para definir las variables de entorno que gustes. Si tu aplicación usa secretos. (credenciales de base de datos, servidores de correo, API keys, etc) se deben configurar no como variables de entorno sino como secretos (key "secrets" del archivo) que se inyectarán a los contenedores como variables de entorno en cuanto el contenedor arranque.
-
Preferir arquitectura Arm64. Si tus imágenes docker son compatibles con arquitectura Arm64 (en vez de x86-64) hay que usar esas para producción, ya que ofrecen mejor relación performance / costo bajo AWS que es donde se ejecutarán.
- La desventaja de esto es que complica tu proceso de build, ya que no podrás usar un simple "docker compose build" o similar para construirlas sino tener un script que use docker buildx para construir y empujar cada imagen. Esto es sencillo una vez que tengas los scripts que lo hagan, pero sí es un detalle adicional a considerar. El proceso para esto se explica en Guía: Cómo construir imágenes docker para la arquitectura Arm64.
Diferencias entre docker compose y el ambiente en AWS ECS
El concepto de "Tarea" (ECS Task y Task Definition)
Cuando ejecutas contenedores docker en AWS Elastic Container Services (AWS ECS) todos se agrupan bajo una "Task" (tarea) que es la que se asigna a uno de los servidores disponibles. La tarea está definida por un "Task Definition" que define qué contenedores y qué recursos asignar a cada contenedor. El task definition sería el equivalente al archivo docker-compose.yml. Más detalles sobre el task definition puede consultar en la documentación oficial sobre Task Definition.
Uno de los contenedores dentro de dicha "Tarea" es el que recibe las peticiones HTTP de parte del balanceador de carga.
Conectarse a otros contenedores
Todos los contenedores dentro de una tarea comparten la misma red, por lo tanto para comunicarse con otro contenedor que está escuchando, por ejemplo, en el puerto 1234
basta conectarse a 127.0.0.1:1234
. Esto es distinto a docker compose, donde se hace referencia a otro contenedor por el nombre del servicio.
Volúmenes
Los volúmenes en AWS ECS son definidiso en el archivo de hosting/deploy_env/task-definition.json
. En general el único volumen que habrá es el que montaremos para almacenamiento persistente si es que la aplicación lo requiere. Ese volumen aparecerá, al igual que con docker compose, como un directorio normal dentro del contenedor.
Ejemplo: docker compose vs AWS ECS
Por ejemplo: si tienes dos contenedores: "nginx" y "php-fpm" y nginx escuchando en el puerto 80 y php-fpm en el 900.
En docker compose:
- Tendrías dos servicios: nginx y php-fpm.
- El servicio Nginx estaría escuchando en el puerto 80, que a la hora de levantar con docker compose up podrías accederlo desde el host: 127.0.0.1:80. Nginx se configuraría en docker-compose.yml para escuchar en el puerto 80:
services:
nginx:
image: nginx:alpine
ports:
- "80:80" - El servicio php-fpm estaría escuchando en el puerto 9000. La configuración de nginx redireccionaría todas las peticiones de PHP usando el nombre del servicio "php-fpm:9000":
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
En Sperantus Hosting que utiliza AWS ECS:
- Correrá una tarea con dos contenedores: nginx y php-fpm
- El servicio Nginx estaría escuchando en el puerto 80, que a la hora de hacer deployment recibirá las peticiones de un balanceador de carga que se encargará de todo el manejo de certificados TLS.
- El contenedor php-fpm estaría escuchando en el puerto 9000. La configuración de nginx redireccionaría todas las peticiones de PHP usando el host 127.0.0.1:9000:
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
} - La configuración de manejo de logs del framework PHP que se esté usando no utiliza un archivo como log sino
php://stderr
. Por ejemplo, con Symfony utilizando Monolog sería algo como lo siguiente:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: php://stderr
level: notice
Criterios para aplicaciones web
-
La conexión segura (TLS) será manejada por un balanceador de carga (incluyendo la renovación del certificado), por lo tanto todas las peticiones llegarán como si fuesen de una misma IP al puerto HTTP de tu aplicación. Según el framework que uses podrías tener que configurar un "trusted proxy" para que use los siguientes HEADERS que el balanceador de carga incluirá en todas las peticiones:
- X-Forwarded-For: IP original que está haciendo el request
- X-Forwarded-Proto: Protocolo del request (siempre será https)
-
Escuchar en el puerto 80, 8080 o 8069. A estos puerto se pueden dirigir las peticiones desde el balanceador de carga. El balanceador se encargará de proveer HTTPS, así que del lado de la aplicación no es necesario hacer nada más.
-
- Si no es posible usar uno de los puertos indicados, coordinarse con Sperantus.
- Si no es posible usar uno de los puertos indicados, coordinarse con Sperantus.
-
-
El URL raíz (/) debe regresar código HTTP 200 si está sano (lo que haga el método internamente depende de la aplicación), de lo contrario el servicio no se reconocerá como sano nunca.
- Idealmente debería haber una ruta como /ping que sirva para verificar la salud de la aplicación e indicar su re-arranque en caso fallo. Si este camino se ve mejor basta indicarlo a Sperantus para configurar así el proyecto.