Хранение статических и мультимедийных файлов Django на Amazon S3

Система хранения данных Amazon Simple Storage System (S3) обеспечивает простой и экономичный способ хранения статических файлов. В этом руководстве показано, как настроить Django на загрузку и обслуживание статических и загруженных пользователем медиафайлов, публичных и частных, через бакет Amazon S3.

Основные зависимости:

  1. Django v4.1.5
  2. Docker v20.10.22
  3. Python v3.11.1

Предпочитаете использовать DigitalOcean Spaces? Посмотрите статью Хранение статических и мультимедийных файлов Django на DigitalOcean Spaces.

S3 Bucket

Перед началом работы вам потребуется учетная запись AWS. Если вы новичок в AWS, Amazon предоставляет бесплатный уровень с 5 ГБ хранилища S3.

Для создания S3 bucket перейдите на S3 страницу и нажмите кнопку "Create bucket":

aws s3

Задайте ведру уникальное, соответствующее DNS имя и выберите регион:

aws s3

В разделе "Владение объектами" выберите "ACLs enabled".

Выключить "Блокировать весь публичный доступ":

aws s3

Создайте ведро. Теперь вы должны увидеть свой бакет на главной странице S3:

aws s3

IAM Access

Хотя можно использовать пользователя AWS root, для безопасности лучше создать пользователя IAM, который будет иметь доступ только к S3 или к определенному ведру S3. Более того, создав группу, гораздо проще назначить (и удалить) доступ к ведру. Итак, мы начнем с создания группы с ограниченными полномочиями, а затем создадим пользователя и назначим его в группу.

IAM Group

В консоли AWS Console перейдите на главную страницу IAM и нажмите кнопку "Группы пользователей" на боковой панели. Затем нажмите кнопку "Создать группу". Укажите имя группы, а затем найдите и выберите встроенную политику "AmazonS3FullAccess":

aws iam

Нажмите кнопку "Создать группу", чтобы завершить настройку группы:

aws iam

Если вы хотите ограничить доступ еще больше, до конкретного ведра, которое мы только что создали, создайте новую политику со следующими разрешениями:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::your-bucket-name",
                "arn:aws:s3:::your-bucket-name/*"
            ]
        }
    ]
}

Обязательно замените your-bucket-name на реальное имя. Затем отсоедините политику "AmazonS3FullAccess" от группы и присоедините новую политику.

IAM User

На главной странице IAM нажмите кнопку "Пользователи", а затем "Добавить пользователя". Задайте имя пользователя и нажмите кнопку "Далее".

aws iam

На шаге "Разрешения" выберите группу, которую мы только что создали:

aws iam

Нажмите кнопку "Создать пользователя", чтобы создать нового пользователя.

aws iam

Теперь щелкните на имени пользователя, чтобы просмотреть его данные. Перейдите на вкладку "Учетные данные безопасности", а затем нажмите кнопку "Создать ключ доступа". Выберите "Локальный код" и нажмите кнопку "Далее".

aws iam

После этого нажмите на кнопку "Создать ключ доступа" и запишите ключи.

aws iam

Проект Django

Склонируйте репо django-docker-s3, а затем проверьте ветку base:

$ git clone https://github.com/testdrivenio/django-docker-s3 --branch base --single-branch
$ cd django-docker-s3

Из корня проекта создайте образы и запустите Docker-контейнеры:

$ docker-compose up -d --build

После завершения сборки соберите статические файлы:

$ docker-compose exec web python manage.py collectstatic

Затем перейдите по адресу http://localhost:1337:

app

Вы должны иметь возможность загрузить изображение, а затем просмотреть его по адресу http://localhost:1337/mediafiles/IMAGE_FILE_NAME.

Не работают радиокнопки для выбора публичного и частного. Мы добавим эту функциональность позже в этом учебнике. Пока же игнорируйте их.

Прежде чем двигаться дальше, взгляните на структуру проекта:

├── .gitignore
├── LICENSE
├── README.md
├── app
│   ├── Dockerfile
│   ├── hello_django
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── manage.py
│   ├── mediafiles
│   ├── requirements.txt
│   ├── static
│   │   └── bulma.min.css
│   ├── staticfiles
│   └── upload
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       │   └── __init__.py
│       ├── models.py
│       ├── templates
│       │   └── upload.html
│       ├── tests.py
│       └── views.py
├── docker-compose.yml
└── nginx
    ├── Dockerfile
    └── nginx.conf

Хотите узнать, как построить этот проект? Ознакомьтесь со статьей Dockerizing Django with Postgres, Gunicorn, and Nginx.

Django Storages

Далее установите django-storages для использования S3 в качестве основного бэкенда хранения данных Django и boto3 для взаимодействия с AWS API.

Обновление файла требований:

boto3==1.26.59
Django==4.1.5
django-storages==1.13.2
gunicorn==20.1.0

Добавьте storages к INSTALLED_APPS в settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'upload',
    'storages',
]

Обновление образов и запуск новых контейнеров:

$ docker-compose up -d --build

Статические файлы

Далее необходимо обновить работу со статическими файлами в settings.py:

STATIC_URL = '/staticfiles/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)


MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')

Замените эти настройки на следующие:

USE_S3 = os.getenv('USE_S3') == 'TRUE'

if USE_S3:
    # aws settings
    AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
    AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
    AWS_DEFAULT_ACL = 'public-read'
    AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
    AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
    # s3 static settings
    AWS_LOCATION = 'static'
    STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/'
    STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
else:
    STATIC_URL = '/staticfiles/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')

Обратите внимание на USE_S3 и STATICFILES_STORAGE:

  1. Переменная окружения USE_S3 используется для включения (значение TRUE) и выключения (значение FALSE) хранилища S3. Таким образом, можно настроить два Docker-композита: один для разработки с выключенным S3, а другой для производства с включенным S3.
  2. Настройка STATICFILES_STORAGE настраивает Django на автоматическое добавление статических файлов в ведро S3 при выполнении команды collectstatic.

Ознакомьтесь с официальной документацией по django-storages для получения дополнительной информации о приведенных выше настройках и конфигурации.

Добавьте соответствующие переменные окружения к сервису web в файле docker-compose.yml:

web:
  build: ./app
  command: bash -c 'while !</dev/tcp/db/5432; do sleep 1; done; gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000'
  volumes:
    - ./app/:/usr/src/app/
    - static_volume:/usr/src/app/staticfiles
    - media_volume:/usr/src/app/mediafiles
  expose:
    - 8000
  environment:
    - SECRET_KEY=please_change_me
    - SQL_ENGINE=django.db.backends.postgresql
    - SQL_DATABASE=postgres
    - SQL_USER=postgres
    - SQL_PASSWORD=postgres
    - SQL_HOST=db
    - SQL_PORT=5432
    - DATABASE=postgres
    - USE_S3=TRUE
    - AWS_ACCESS_KEY_ID=UPDATE_ME
    - AWS_SECRET_ACCESS_KEY=UPDATE_ME
    - AWS_STORAGE_BUCKET_NAME=UPDATE_ME
  depends_on:
    - db

Не забудьте обновить AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY только что созданными ключами пользователей вместе с AWS_STORAGE_BUCKET_NAME.

Для тестирования, пересборки и запуска контейнеров:

$ docker-compose down -v
$ docker-compose up -d --build

Соберите статические файлы:

$ docker-compose exec web python manage.py collectstatic

Это должно занять гораздо больше времени, чем раньше, поскольку файлы загружаются в ведро S3.

http://localhost:1337 по-прежнему должен отображаться корректно:

app

Просмотрите источник страницы, чтобы убедиться, что таблица стилей CSS извлекается из корзины S3:

app

Убедитесь, что статические файлы видны на консоли AWS в подпапке "static" ведра S3:

aws s3

Загрузка медиафайлов по-прежнему будет происходить в локальную файловую систему, поскольку мы настроили S3 только для статических файлов. С загрузкой медиафайлов мы поработаем в ближайшее время.

Наконец, измените значение USE_S3 на FALSE и пересоберите образы, чтобы убедиться, что Django использует локальную файловую систему для статических файлов. После этого измените значение USE_S3 обратно на TRUE.

Публичные медиафайлы

Для того чтобы пользователи не перезаписывали существующие статические файлы, загружаемые медиафайлы следует помещать в другую подпапку в ведре. Для этого мы создадим собственные классы хранения для каждого типа хранилища.

Добавьте новый файл с именем storage_backends.py в папку "app/hello_django":

from django.conf import settings
from storages.backends.s3boto3 import S3Boto3Storage


class StaticStorage(S3Boto3Storage):
    location = 'static'
    default_acl = 'public-read'


class PublicMediaStorage(S3Boto3Storage):
    location = 'media'
    default_acl = 'public-read'
    file_overwrite = False

Внесите следующие изменения в файл settings.py:

USE_S3 = os.getenv('USE_S3') == 'TRUE'

if USE_S3:
    # aws settings
    AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
    AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
    AWS_DEFAULT_ACL = None
    AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
    AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
    # s3 static settings
    STATIC_LOCATION = 'static'
    STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/'
    STATICFILES_STORAGE = 'hello_django.storage_backends.StaticStorage'
    # s3 public media settings
    PUBLIC_MEDIA_LOCATION = 'media'
    MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_MEDIA_LOCATION}/'
    DEFAULT_FILE_STORAGE = 'hello_django.storage_backends.PublicMediaStorage'
else:
    STATIC_URL = '/staticfiles/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    MEDIA_URL = '/mediafiles/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')

STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

Теперь, когда параметр DEFAULT_FILE_STORAGE setting установлен, все FileField будут загружать свое содержимое в ведро S3. Просмотрите оставшиеся настройки, прежде чем двигаться дальше.

Далее внесем несколько изменений в приложение upload.

app/upload/models.py:

from django.db import models


class Upload(models.Model):
    uploaded_at = models.DateTimeField(auto_now_add=True)
    file = models.FileField()

app/upload/views.py:

from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render

from .models import Upload


def image_upload(request):
    if request.method == 'POST':
        image_file = request.FILES['image_file']
        image_type = request.POST['image_type']
        if settings.USE_S3:
            upload = Upload(file=image_file)
            upload.save()
            image_url = upload.file.url
        else:
            fs = FileSystemStorage()
            filename = fs.save(image_file.name, image_file)
            image_url = fs.url(filename)
        return render(request, 'upload.html', {
            'image_url': image_url
        })
    return render(request, 'upload.html')

Создайте новый файл миграции, а затем постройте новые образы:

$ docker-compose exec web python manage.py makemigrations
$ docker-compose down -v
$ docker-compose up -d --build
$ docker-compose exec web python manage.py migrate

Протестируйте его! Загрузите изображение по адресу http://localhost:1337. Изображение должно быть загружено в S3 (в подпапку media), а image_url должно содержать S3 url:

app demo

Приватные медиафайлы

Добавить новый класс в storage_backends.py:

class PrivateMediaStorage(S3Boto3Storage):
    location = 'private'
    default_acl = 'private'
    file_overwrite = False
    custom_domain = False

Добавьте соответствующие настройки:

USE_S3 = os.getenv('USE_S3') == 'TRUE'

if USE_S3:
    # aws settings
    AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
    AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
    AWS_DEFAULT_ACL = None
    AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
    AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
    # s3 static settings
    STATIC_LOCATION = 'static'
    STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/'
    STATICFILES_STORAGE = 'hello_django.storage_backends.StaticStorage'
    # s3 public media settings
    PUBLIC_MEDIA_LOCATION = 'media'
    MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{PUBLIC_MEDIA_LOCATION}/'
    DEFAULT_FILE_STORAGE = 'hello_django.storage_backends.PublicMediaStorage'
    # s3 private media settings
    PRIVATE_MEDIA_LOCATION = 'private'
    PRIVATE_FILE_STORAGE = 'hello_django.storage_backends.PrivateMediaStorage'
else:
    STATIC_URL = '/staticfiles/'
    STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
    MEDIA_URL = '/mediafiles/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')

STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

Создайте новую модель в app/upload/models.py:

from django.db import models

from hello_django.storage_backends import PublicMediaStorage, PrivateMediaStorage


class Upload(models.Model):
    uploaded_at = models.DateTimeField(auto_now_add=True)
    file = models.FileField(storage=PublicMediaStorage())


class UploadPrivate(models.Model):
    uploaded_at = models.DateTimeField(auto_now_add=True)
    file = models.FileField(storage=PrivateMediaStorage())

Затем обновляем представление:

from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render

from .models import Upload, UploadPrivate


def image_upload(request):
    if request.method == 'POST':
        image_file = request.FILES['image_file']
        image_type = request.POST['image_type']
        if settings.USE_S3:
            if image_type == 'private':
                upload = UploadPrivate(file=image_file)
            else:
                upload = Upload(file=image_file)
            upload.save()
            image_url = upload.file.url
        else:
            fs = FileSystemStorage()
            filename = fs.save(image_file.name, image_file)
            image_url = fs.url(filename)
        return render(request, 'upload.html', {
            'image_url': image_url
        })
    return render(request, 'upload.html')

Снова создаем файл миграции, пересобираем образы и запускаем новые контейнеры:

$ docker-compose exec web python manage.py makemigrations
$ docker-compose down -v
$ docker-compose up -d --build
$ docker-compose exec web python manage.py migrate

Для проверки загрузите частное изображение по адресу http://localhost:1337. Как и публичное изображение, оно должно быть загружено в S3 (в приватную подпапку), а image_url должен включать URL S3 вместе со следующими параметрами строки запроса:

  1. AWSAccessKeyId
  2. Signature
  3. Expires

По сути, мы создали временный, подписанный URL, к которому пользователи могут обращаться в течение определенного времени. Вы не сможете получить к нему доступ напрямую, без параметров.

app demo

Заключение

В этом руководстве мы рассмотрели, как создать ведро на Amazon S3, настроить пользователя и группу IAM, а также настроить Django на загрузку и обслуживание статических файлов и медиафайлов в S3 и из него.

Используя S3, вы:

  1. Увеличивается объем свободного места для статических и мультимедийных файлов
  2. Снизить нагрузку на собственный сервер, поскольку ему больше не нужно обслуживать файлы
  3. Можно ограничить доступ к определенным файлам
  4. Возможность использования преимуществ CloudFront CDN

Сообщите нам, если мы что-то упустили, или если у вас есть другие советы и рекомендации. Окончательный вариант кода можно найти в репозитории django-docker-s3.

Вернуться на верх