Хранение статических и мультимедийных файлов Django на Amazon S3
Система хранения данных Amazon Simple Storage System (S3) обеспечивает простой и экономичный способ хранения статических файлов. В этом руководстве показано, как настроить Django на загрузку и обслуживание статических и загруженных пользователем медиафайлов, публичных и частных, через бакет Amazon S3.
Основные зависимости:
- Django v4.1.5
- Docker v20.10.22
- Python v3.11.1
Предпочитаете использовать DigitalOcean Spaces? Посмотрите статью Хранение статических и мультимедийных файлов Django на DigitalOcean Spaces.
S3 Bucket
Перед началом работы вам потребуется учетная запись AWS. Если вы новичок в AWS, Amazon предоставляет бесплатный уровень с 5 ГБ хранилища S3.
Для создания S3 bucket перейдите на S3 страницу и нажмите кнопку "Create bucket":
Задайте ведру уникальное, соответствующее DNS имя и выберите регион:
В разделе "Владение объектами" выберите "ACLs enabled".
Выключить "Блокировать весь публичный доступ":
Создайте ведро. Теперь вы должны увидеть свой бакет на главной странице S3:
IAM Access
Хотя можно использовать пользователя AWS root, для безопасности лучше создать пользователя IAM, который будет иметь доступ только к S3 или к определенному ведру S3. Более того, создав группу, гораздо проще назначить (и удалить) доступ к ведру. Итак, мы начнем с создания группы с ограниченными полномочиями, а затем создадим пользователя и назначим его в группу.
IAM Group
В консоли AWS Console перейдите на главную страницу IAM и нажмите кнопку "Группы пользователей" на боковой панели. Затем нажмите кнопку "Создать группу". Укажите имя группы, а затем найдите и выберите встроенную политику "AmazonS3FullAccess":
Нажмите кнопку "Создать группу", чтобы завершить настройку группы:
Если вы хотите ограничить доступ еще больше, до конкретного ведра, которое мы только что создали, создайте новую политику со следующими разрешениями:
{ "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 нажмите кнопку "Пользователи", а затем "Добавить пользователя". Задайте имя пользователя и нажмите кнопку "Далее".
На шаге "Разрешения" выберите группу, которую мы только что создали:
Нажмите кнопку "Создать пользователя", чтобы создать нового пользователя.
Теперь щелкните на имени пользователя, чтобы просмотреть его данные. Перейдите на вкладку "Учетные данные безопасности", а затем нажмите кнопку "Создать ключ доступа". Выберите "Локальный код" и нажмите кнопку "Далее".
После этого нажмите на кнопку "Создать ключ доступа" и запишите ключи.
Проект 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:
Вы должны иметь возможность загрузить изображение, а затем просмотреть его по адресу 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
:
- Переменная окружения
USE_S3
используется для включения (значениеTRUE
) и выключения (значениеFALSE
) хранилища S3. Таким образом, можно настроить два Docker-композита: один для разработки с выключенным S3, а другой для производства с включенным S3. - Настройка
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 по-прежнему должен отображаться корректно:
Просмотрите источник страницы, чтобы убедиться, что таблица стилей CSS извлекается из корзины S3:
Убедитесь, что статические файлы видны на консоли AWS в подпапке "static" ведра 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:
Приватные медиафайлы
Добавить новый класс в 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 вместе со следующими параметрами строки запроса:
- AWSAccessKeyId
- Signature
- Expires
По сути, мы создали временный, подписанный URL, к которому пользователи могут обращаться в течение определенного времени. Вы не сможете получить к нему доступ напрямую, без параметров.
Заключение
В этом руководстве мы рассмотрели, как создать ведро на Amazon S3, настроить пользователя и группу IAM, а также настроить Django на загрузку и обслуживание статических файлов и медиафайлов в S3 и из него.
Используя S3, вы:
- Увеличивается объем свободного места для статических и мультимедийных файлов
- Снизить нагрузку на собственный сервер, поскольку ему больше не нужно обслуживать файлы
- Можно ограничить доступ к определенным файлам
- Возможность использования преимуществ CloudFront CDN
Сообщите нам, если мы что-то упустили, или если у вас есть другие советы и рекомендации. Окончательный вариант кода можно найти в репозитории django-docker-s3.
Вернуться на верх