Como ya habréis notado, los servicios en la nube están muy de moda. Al final son servicios que nos quitan la responsabilidad de mantener hardware, redes, almacenamiento físico, etc. para que nos centremos en lo que realmente importa, dar soluciones de software de calidad.
Pues bien, en esta publicación veremos el como alojar contenido multimedia (imágenes en nuestro ejemplo) en un bucket S3 de AWS. A modo muy resumido, el servicio S3 de AWS nos permite alojar cualquier tipo de fichero (u objeto, como indica AWS en su documentación), siendo el volumen totalmente flexible. Es decir, no nos tenemos que preocupar de crear un volumen con un espacio en concreto, sino que dicho volumen va aumentando o disminuyendo de tamaño en función de la cantidad de ficheros que vaya conteniendo.
Además de la escalabilidad indicada, S3 ofrece seguridad y rendimiento, pero lo mejor es que echéis un ojo a su documentación (si no habéis usado antes este servicio) para que estéis al tanto de todas sus bondades.
Dicho esto, vamos a ir viendo punto por punto todo lo necesario para que nuestra aplicación web en Django pueda persistir las imágenes en un bucket S3.
Tabla de contenido
- Crear el bucket S3 en AWS
- Crear usuario y conceder permisos para interactuar con el bucket
- Configurar Django para persistir imágenes en el bucket
- Siguientes pasos
- Conclusión
Crear el bucket S3 en AWS
Dos aclaraciones importantes antes de ponernos manos a la obra.
- Es necesario disponer de una cuenta de AWS para poder crear y gestionar el bucket S3. Si no tenéis una cuenta creada, os la podéis crear desde aquí.
- AWS es un servicio de pago, con lo que crear y mantener un bucket S3 conlleva una serie de costes que pueden variar en función de la cantidad de espacio que vayáis a usar en dicho bucket. Si os habéis tenido que crear una cuenta en el punto 1, habréis visto que AWS ofrece un plan gratuito durante 12 meses. Aún así, os recomiendo que leáis detenidamente su plan de precios para evitar cualquier tipo de sorpresa.
- Sería importante tener un mínimo de conocimiento de AWS, para al menos entender la importancia de las regiones, ya que al principio puede parecer un poco “enrevesado”. En el ejemplo que veremos usaremos la región eu-west-1 (Irlanda).
Ahora sí, dentro de nuestra consola en AWS (y estando activa la región de Irlanda) buscamos el servicio S3. En el menú lateral izquierdo pulsamos sobre la primera opción “Buckets” y seguidamente en la parte de la derecha pulsamos sobre “Crear bucket”.
En la siguiente pantalla tendremos que indicar el nombre del bucket, el cual tiene que ser único dentro del espacio de nombres local. En mi caso indicaré “pythoninspanish-bucket-s3-a3ed21”. El resto de configuraciones las dejaremos por defecto. Existen ciertas opciones muy interesantes, como el control al acceso público o el control de versiones de los ficheros que vayamos a alojar en el bucket, pero no entraremos a verlo en detalle ya que se aleja del fin del post.
Una vez indicado el nombre, le damos a “Crear bucket” y listo, ya tendremos nuestro bucket creado.
Crear usuario y conceder permisos para interactuar con el bucket
Ahora que ya tenemos nuestro bucket creado en AWS, necesitaremos crear un usuario con el que nuestra aplicación Django pueda interactuar con el bucket. Para ello usaremos el servicio IAM para crear un usuario.
Buscamos el servicio IAM, y en el panel izquierdo seleccionamos “Usuarios” dentro del desplegable “Administración del acceso”. Seguidamente pulsamos sobre el botón “Crear usuario” situado en la parte superior derecha.
En nuestro ejemplo usaremos el nombre “django-app”, y pulsamos “Siguiente”. No es necesario seleccionar la casilla “Proporcionar acceso de usuario a la consola de administración de AWS”, ya que la única finalidad del usuario que estamos creando es la de interactuar con AWS a través de código Python, en concreto usando la librería boto3; esto ya lo veremos en el siguiente apartado.
En la siguiente pantalla que se nos muestra debemos establecer los permisos que tendrá nuestro usuario. AWS nos ofrece tres formas diferentes de aplicar permisos; mediante un grupo, copiando los permisos de otro usuario ya existente, o indicando directamente los permisos explícitamente. La forma más correcta y fácil de mantener es la de añadir a los diferentes usuarios en grupos, y establecer a dichos usuarios los permisos necesarios en función de los servicios que tenga que utilizar. Dicho esto, en nuestro ejemplo estableceremos directamente los permisos necesarios al nuevo usuario, ya que para seguir con el ejemplos nos és más que suficiente.
Pues bien, seleccionamos “Adjuntar políticas directamente”, y seguidamente buscamos la política “AmazonS3FullAccess”. Dicha política permite al usuario poder crear, modificar y eliminar cualquier objeto dentro del bucket S3. Seleccionamos la política y pulsamos en “Siguiente”.
En la siguiente pantalla pulsamos en “Crear usuario” y ya tendremos nuestro usuario creado con los permisos indicados.
En este momento ya tenemos el usuario creado, pero nos faltaría crear los credenciales para poder usarlos en nuestra aplicación Django. Para ello tendremos que ir a IAM → Administración del acceso → Usuario y pulsamos sobre el usuario recién creado. Una vez dentro, en la pestaña “Credenciales de seguridad” pulsamos en “Crear clave de acceso” dentro del apartado “Claves de acceso”.
En el caso de uso a seleccionar debéis tener en cuenta la finalidad real de cada una de vuestras aplicaciones. Por ejemplo, si vuestra app Django se va a ejecutar dentro de un servicio de AWS deberéis seleccionar “Aplicación ejecutada en un servicio de computación de AWS”. O si por ejemplo la clave de acceso la fueseis a usar para interactuar con CLI de AWS podríais seleccionar la primera opción, “Interfaz de línea de comandos (CLI)”. En este ejemplo usaremos “Código local”, pero lo dicho, escoged la que más se ajuste a vuestro caso real.
En el segundo paso pulsamos en “Crear clave de acceso”, y paso seguido nos aseguramos de copiar y guardarnos a buen recaudo la clave de acceso y su correspondiente secret.
En este punto ya disponemos de los credenciales necesarios para que desde nuestra app Django podamos subir ficheros en el bucket S3 usando el usuario “django-app”. Veamos a continuación como configurar nuestra app para ello.
Configurar Django para persistir imágenes en el bucket
Bien, ya tenemos por una parte el bucket en el S3 para alojar los ficheros que necesitemos, y por otra un usuario con sus correspondientes claves de acceso para usarlos desde nuestra app Django. Así que vamos a ponernos manos a la obra para ver todo esto en funcionamiento.
Van a ser necesarias unas cuantas dependencias de librerías Python para poder integrar nuestra app Django con AWS.
pip install boto3 # AWS SDK
pip install django-storages # colección de diferentes storages para Django
pip install Pillow # librería para poder usar campos de tipo ImageField
Además tendremos que realizar una configuración en el fichero settings.py de nuestro proyecto, para indicar una serie de variables para poder realizar la comunicación entre nuestra app y el bucket S3 de AWS.
# settings.py
import os
DEBUG = False
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY")
AWS_S3_REGION_NAME = os.environ.get("AWS_S3_REGION_NAME", "eu-west-1")
AWS_STORAGE_BUCKET_NAME = os.environ.get("S3_BUCKET")
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/media/"
if DEBUG:
MEDIA_URL = "/media/"
Así de entrada os habréis fijado que todas las constantes del settings.py las obtenemos de variables de entorno. Esto no es obligatorio, pero evidentemente son buenas prácticas para no dejar información sensible directamente en nuestro código base.
Cabe destacar que el nombre de las constantes deben ser exactamente como las indicadas en el ejemplo (salvo AWS_STORAGE_BUCKET_NAME
y AWS_S3_CUSTOM_DOMAIN
que las usamos para construir el MEDIA_URL
), de lo contrario Django no se podrá conectar a AWS, y en consecuencia al bucket S3. Dicho esto, tan solo tendríamos que establecer las variables de entorno correspondientes con los valores de cada uno.
Por último nos quedaría configurar nuestro modelo para que automáticamente aloje las imágenes al bucket S3. En el ejemplo de a continuación, veremos un modelo en el que persistiremos el título y contenido de una publicación, y en el que además usaremos un campo de tipo ImageField para alojar la imagen de portada de dicha publicación.
El modelo se vería tal que así:
# models.py
import uuid
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.db import models
from storages.backends.s3boto3 import S3Boto3Storage
def image_file_name(instance: models.Model, filename: str) -> str:
return f"posts/{uuid.uuid4()}.jpg"
if settings.DEBUG:
class PostImageStorage(FileSystemStorage):
pass
else:
class PostImageStorage(S3Boto3Storage):
location = "media"
file_overwrite = True
class Post(models.Model):
title = models.CharField(
verbose_name="Título",
max_length=255,
)
content = models.TextField(
verbose_name="Contenido",
)
image = models.ImageField(
verbose_name="Imagen",
upload_to=image_file_name,
storage=PostImageStorage(),
null=True,
blank=True,
)
class Meta:
verbose_name = "Publicación"
verbose_name_plural = "Publicaciones"
ordering = ("-id",)
def __str__(self) -> str:
return self.title
Hablemos en detalle por los puntos más importantes:
- Si os fijáis la clase
PostImageStorage
es creada de una forma u otra en función de si la app Django se ejecuta en modoDEBUG
. Esto no es obligatorio, pero sería una buena práctica el usar el almacenamiento de AWS únicamente para entornos de producción. Dicho esto, la diferencia entre ambas clases es que si estamos en modoDEBUG
el sistema de almacenaje sería el host local del servidor que se encargue de ejecutar la app Django (FileSystemStorage
), o el almacenamiento del servicio S3 de AWS (S3Boto3Storage
). En caso de éste último además indicamos un par de atributos extra:location = “media”
se establece el directorio raíz del bucket S3 donde se alojarán las imágenes.file_overwrite = True
en caso de que se intente subir un fichero con un nombre ya existente en el bucket, éste será reemplazado por el nuevo que se suba.
- La función
image_file_name
la usamos en el atributoupload_to
del campo image. La función simplemente se encarga de montar el nombre que tendrá la imagen cuando se aloje en el almacenamiento correspondiente. Por cierto, no os preocupéis si los directorios no existen en el almacenamiento de destino, ya sea el local o el bucket S3. Si no existe el directorio posts, éste se creará la primera vez que se aloje una imagen.
Nada más reseñable en la definición del modelo. Sólo un pequeño detalle, vamos a registrar el modelo en el admin site de Django para poder realizar las pruebas de una forma sencilla.
# admin.py
from django.contrib import admin
from blog.models import Post # sustituir por el nombre de vuestra app
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = (
"pk",
"title",
)
Pues bien, una vez creadas y aplicadas las migraciones del nuevo modelo, si accedemos al administrador de Django podremos visualizar el nuevo menú “Publicaciones”. Creamos un registro nuevo rellenando los campos con el contenido que queráis. Aquí el campo que nos concierne es el “Imagen”, donde escogeremos cualquier imagen que tengamos en nuestro equipo. Una vez listo, lo guardamos.
Y aquí es donde se ha realizado la magia, en el momento de guardar el objeto y tener configurado el campo de nuestro modelo como almacenaje el servicio S3, automáticamente se habrá subido a AWS. Si accedemos al bucket S3 podremos verlo.
Un par de cosas importantes a tener en cuenta:
- Fijaros como la imagen se ha alojado bajo la ruta de directorios media/posts/. Esto es así debido a que por una parte hemos indicado en nuestro settings de Django (el atributo
MEDIA_ROOT
) que el directorio base para alojar el contenido multimedia es el directorio media. Y por otra en que en la definición del campo image de nuestro modeloPost
se ha indicado el atributo*upload_to*
(comentado anteriormente). - Inicialmente al crear el bucket S3 la estructura de directorio media/posts/ no existía, pero como habéis visto esto no supone un problema, ya que automáticamente se crean los directorios necesarios para alojar el fichero en la ruta indicada.
Siguientes pasos
En la publicación hemos pasado varios puntos por encima sin entrar mucho al “barro”, pero ahora que ya tenéis la idea general de como realizar la comunicación entre una app Django y un bucket S3, podréis investigar y practicar con ciertos puntos. Incluso implementar ciertas buenas prácticas que serían convenientes en cualquier proyecto. Os comento algunos aspectos interesantes a investigar.
- Rotar credenciales de AWS. En nuestro ejemplo hemos visto el como crear un usuario para poder interactuar con el SDK de AWS a través de credenciales. Sería conveniente tener una política de rotado de credenciales, para que cada x tiempo se creen nuevos y se eliminen los existentes. En empresas grandes suelen haber equipos de devops orientados a seguridad que se encargarían de ello, pero si se trata de una empresa pequeña o incluso de un proyecto propio, tendréis que hacerlo vosotros mismos.
- No dejar que el bucket S3 sea un sumidero de imágenes innecesarias. Hemos configurado el alojamiento de nuestro campo “image” para que automáticamente se suba la imagen al bucket, ¿pero qué ocurre si cambiamos en nuestro objeto la image? ¿O si eliminamos el objeto? Las imágenes alojadas seguirán existiendo en nuestro bucket, cuando en realidad no tendría ningún tipo de sentido. Existen métodos para controlar dicho comportamiento, como por ejemplo extender el método
save
de nuestro objeto para poder eliminar la imagen de nuestro bucket S3. Os dejo un ejemplo:
def save(self, *args, **kwargs) -> None:
try:
post = Post.objects.get(id=self.id)
except Post.DoesNotExist:
pass
else:
# the image has been removed from the post
if post.image and not self.image:
post.image.delete(save=False) # this will delete image from bucket
# the image has been updated
elif post.image != self.image:
post.image.delete(save=False) # this will delete image from bucket
super().save(*args, **kwargs)
- Subir/bajar cualquier tipo de fichero en el bucket. En el ejemplo visto hemos configurado nuestro modelo para alojar automáticamente las imágenes de nuestro modelo en el bucket. Pero podemos tener la necesidad de alojar cualquier tipo de fichero en cualquier proceso interno, o de bajarnos cualquier fichero para procesarlo y realizar la tarea que necesitemos. En estos casos no se configuraría la conexión con el almacenaje como hemos visto en nuestro modelo, sino más bien tendríamos que realizar la conexión usando directamente el SDK de AWS para Python (librería boto3) para realizar dicha interacción. Podéis encontrar toda la información necesaria en su documentación oficial.
Conclusión
Como habéis visto la configuración de Django para alojar contenido multimedia en un bucket S3 de AWS es relativamente sencilla, pero extremadamente versátil para usarlo como alojamiento para nuestros proyectos. Nos da la facilidad de no tener que preocuparnos de tener un servidor dedicado para ello, evitando tareas de mantenimiento en las que en ciertas ocasiones pueden ser bastante tediosas.
La latencia como tal es muy baja, con lo que el rendimiento en nuestros proyectos no se verá afectado. Sobre todo si tenemos en cuenta ciertas buenas prácticas en la creación de los bucket y en qué regiones ubicarlos.
Como siempre, espero que os haya sido de utilidad lo visto en esta publicación. Hasta la próxima, ¡pythonistas! 🐍❤️