Это пятая часть руководства «Развертывание приложения Django на AWS с помощью Terraform». С предыдущими шагами вы можете ознакомиться здесь:
- Часть 1: Минимальная рабочая установка
- Часть 2: Подключение PostgreSQL RDS
- Часть 3: GitLab CI/CD
- Часть 4: Домен Namecheap + SSL
В этом шаге мы собираемся:
- Добавим настройку Celery + SQS для локальной разработки.
- Создадим периодическую задачу с помощью Celery Beat.
- Добавим создание экземпляра SQS на AWS.
- Развернуть рабочий и битовый ECS-сервисы на AWS.
О Celery и SQS
Как сказано в документации:
Celery — это простая, гибкая и надежная распределенная система для обработки огромного количества сообщений, предоставляющая операторам инструменты, необходимые для обслуживания такой системы. Это очередь задач, ориентированная на обработку в реальном времени, но при этом поддерживающая планирование задач.
Таким образом, вы можете запускать свои длительные задачи, требующие большого количества CPU и IO, в отдельных контейнерах docker. Ваш веб-сервер может запланировать некоторые задачи, а Celery будет выбирать и выполнять их.
Веб-сервер и Celery взаимодействуют через некоторый бэкенд. Это может быть Redis или RabbitMQ. Но мы используем AWS в качестве облачного провайдера. Поэтому мы можем использовать бэкенд SQS. В документации по Celery говорится:
Если вы уже тесно интегрированы с AWS и знакомы с SQS, он представляет собой отличный вариант в качестве брокера. Он чрезвычайно масштабируемый и полностью управляемый, и управляет делегированием задач аналогично RabbitMQ.
Более подробную информацию о SQS вы можете найти здесь.
Локальная установка для разработки
Запуск SQS локально
SQS — это управляемое решение от AWS. И мы можем создать отдельный экземпляр SQS для целей разработки. Но я хочу запускать все локально, чтобы иметь возможность работать без подключения к Интернету. Кроме того, это хорошая практика для запуска модульных и интеграционных тестов без доступа к Интернету.
Для локального запуска SQS мы воспользуемся Docker-образом softwaremill/elasticmq-native.
Перейдите в папку django-aws-backend
и добавьте новый сервис в docker-compose.yml
в вашем проекте Django:
...
services:
...
sqs:
image: "softwaremill/elasticmq-native:latest"
ports:
- "9324:9324"
- "9325:9325"
Запустите docker-compose up -d
для запуска контейнера SQS. Затем проверьте http://127.0.0.1:9325/ в вашем браузере, чтобы увидеть панель управления SQS.
Также проверьте URL http://127.0.0.1:9324/, чтобы убедиться, что SQS API работает. Вы увидите XML-вывод ошибки, как показано ниже:
<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
<Error>
<Type>Sender</Type>
<Code>MissingAction</Code>
<Message>MissingAction; see the SQS docs.</Message>
<Detail/>
</Error>
<RequestId>00000000-0000-0000-0000-000000000000</RequestId>
</ErrorResponse>
Теперь мы готовы добавить Celery в наш проект Django.
Добавление Celery в проект Django
Давайте добавим пакет Celery в requirements.txt
и запустим pip install -r requirements.txt
. Убедитесь, что перед этим вы активировали venv
.
celery[sqs]==5.2.6
Затем создайте новый файл django_aws/celery.py
со следующим содержимым:
import os
from celery import Celery
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_aws.settings")
app = Celery("django_aws")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
и добавьте в django_aws/settings.py
эти строки:
CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="sqs://localhost:9324")
CELERY_TASK_DEFAULT_QUEUE = env("CELERY_TASK_DEFAULT_QUEUE", default="default")
CELERY_BROKER_TRANSPORT_OPTIONS = {
"region": env("AWS_REGION", default="us-east-1")
}
Здесь мы инициализировали приложение Celery в django_aws/celery.py
. Мы будем использовать это приложение для определения и планирования задач для Celery. Также мы задали параметры подключения в django_aws/settings.py
. В качестве значений по умолчанию мы установили нашу локальную настройку. Для производства мы можем передавать параметры через переменные окружения.
Теперь мы готовы к созданию и запуску нашей первой задачи. Давайте создадим django_aws/tasks.py
со следующим кодом:
import logging
import time
from django_aws import celery
@celery.app.task()
def web_task() -> None:
logging.info("Starting web task...")
time.sleep(10)
logging.info("Done web task.")
Задача web_task
будет выполняться в течение 10 секунд и помещать сообщения в поток журнала в начале и в конце выполнения.
Теперь нам нужно добавить способ добавления этой задачи в очередь. Давайте создадим django_aws/views.py
со следующим view
:
from django.http import HttpResponse
from django_aws.tasks import web_task
def create_web_task(request):
web_task.delay()
return HttpResponse("Task added")
Добавим это представление в urls.py
:
...
from django_aws import views
urlpatterns = [
...
path('create-task', views.create_web_task),
]
...
Теперь, если мы нажмем URL http://127.0.0.1:8000/create-task, представление create_web_task
добавит новую задачу в локальный SQS. Запустите локальный веб-сервер с помощью python manage.py runserver
, несколько раз перейдите по этому URL и посмотрите на страницу администратора SQS http://127.0.0.1:9325/.
Итак, мы успешно добавили задания в очередь. Теперь давайте выполним их с помощью celery. Запустите celery -A django_aws worker --loglevel info
, чтобы запустить рабочий процесс. Рабочий немедленно выберет задания из очереди и выполнит их:
Остановите процесс celery
.
Если вы столкнулись с проблемой
ImportError: Клиент curl требует библиотеки pycurl
, посмотрите мой пост на StackOverflow.
Также нам нужно добавить некоторые библиотеки в Dockerfile
для компиляции pycurl
в образ докера. Замените Dockerfile
на следующий:
FROM python:3.10-slim-buster
EXPOSE 8000
ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update
&& apt-get --no-install-recommends install -y
build-essential
libssl-dev
libcurl4-openssl-dev
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir --upgrade pip
RUN pip install gunicorn==20.1.0
COPY requirements.txt /
RUN pip install --no-cache-dir -r /requirements.txt
WORKDIR /app
COPY . /app
RUN ./manage.py collectstatic --noinput
Добавление Celery beat
Теперь давайте создадим периодическую задачу с помощью Celery Beat. Мы добавим простую задачу типа create_web_task
и запланируем ее выполнение раз в минуту. Для этого добавим beat_task
в tasks.py
:
@celery.app.task()
def beat_task() -> None:
logging.info("Starting beat task...")
time.sleep(10)
logging.info("Done beat task.")
Затем добавим настройку CELERY_BEAT_SCHEDULE в settings.py
:
from datetime import timedelta
...
CELERY_BEAT_SCHEDULE = {
"beat_task": {
"task": "django_aws.tasks.beat_task",
"schedule": timedelta(minutes=1),
},
}
и запустите процесс beat celery -A django_aws beat --loglevel info
. Каждую минуту процесс beat
добавляет новую задачу в SQS. Проверьте http://127.0.0.1:9325/, чтобы увидеть их.
Подождите, пока несколько задач встанут в очередь, остановите процесс beat
и снова запустите рабочий celery -A django_aws worker --loglevel info
. Рабочий обработает задания beat_task
, и вы увидите журналы:
[2022-08-04 11:13:59,088: INFO/MainProcess] Task django_aws.tasks.beat_task[4189aa07-b75e-4743-94e0-2a0c3b84443a] received
[2022-08-04 11:13:59,089: INFO/MainProcess] Task django_aws.tasks.beat_task[0de67363-2e2a-421c-9630-1c6c7c685382] received
[2022-08-04 11:13:59,095: INFO/ForkPoolWorker-1] Starting beat task...
[2022-08-04 11:13:59,095: INFO/ForkPoolWorker-8] Starting beat task...
[2022-08-04 11:14:09,096: INFO/ForkPoolWorker-1] Done beat task.
[2022-08-04 11:14:09,096: INFO/ForkPoolWorker-8] Done beat task.
[2022-08-04 11:14:09,097: INFO/ForkPoolWorker-1] Task django_aws.tasks.beat_task[0de67363-2e2a-421c-9630-1c6c7c685382] succeeded in 10.002475121000316s: None
[2022-08-04 11:14:09,097: INFO/ForkPoolWorker-8] Task django_aws.tasks.beat_task[4189aa07-b75e-4743-94e0-2a0c3b84443a] succeeded in 10.002584206999018s: None
Итак, мы успешно запустили Celery worker и победили процессы, используя локальный SQS. Давайте добавим файл celerybeat-schedule
в .gitignore
, зафиксируем и продвинем наши изменения. Убедитесь, что CI/CD прошел успешно.
Мы закончили с частью Django, давайте перейдем к AWS.
Развертывание на AWS
Создание экземпляра и пользователя AWS SQS
Перейдите в папку django-aws-infrastructure
, создайте файл sqs.tf
со следующим содержимым и запустите terraform apply
.
resource "aws_sqs_queue" "prod" {
name = "prod-queue"
receive_wait_time_seconds = 10
tags = {
Environment = "production"
}
}
resource "aws_iam_user" "prod_sqs" {
name = "prod-sqs-user"
}
resource "aws_iam_user_policy" "prod_sqs" {
user = aws_iam_user.prod_sqs.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"sqs:*",
]
Effect = "Allow"
Resource = "arn:aws:sqs:*:*:*"
},
]
})
}
resource "aws_iam_access_key" "prod_sqs" {
user = aws_iam_user.prod_sqs.name
}
Здесь мы создали новый экземпляр SQS, нового пользователя IAM, предоставили доступ к этому экземпляру SQS этому пользователю и создали ключ доступа IAM для предоставления доступа к SQS из приложения Django. Давайте посмотрим на новый экземпляр в консоли AWS:
Активация региона
Теперь давайте создадим ECS для Celery worker и beat.
Во-первых, давайте «активируем» наш регион на AWS. По какой-то причине AWS не позволяет создавать более 2 контейнеров ECS в регионе. Чтобы снять это ограничение, нужно создать экземпляр EC2 в этом регионе. Давайте сделаем это вручную в EC2 Console. Убедитесь, что вы используете свой регион AWS.
- Нажмите «Запустить экземпляр».
- Выберите любое имя для вашего экземпляра. Я выберу
Test Server
.
- Прокрутите вниз до карточки «Key pair» и выберите «Proceed without a key pair». Мы не будем подключаться к этому серверу, поэтому она нам не нужна. Затем нажмите «Launch Instance», чтобы создать новый экземпляр в вашем регионе.
AWS вскоре создаст экземпляр. Перейдите на вкладку Instances в EC2 Console и убедитесь, что у вас есть 1 «Running» экземпляр.
После этого вы можете завершить работу экземпляра, поскольку он нам не нужен. Выберите экземпляр, нажмите «Состояние экземпляра», затем «Прекратить экземпляр» и подтвердите прекращение. AWS навсегда удалит ваш экземпляр EC2.
Итак, теперь мы можем создавать более двух контейнеров ECS. Давайте продолжим создание Celery ECS.
Запуск Celery через ECS
Теперь давайте определим наш сервис Celery ECS. Сначала добавьте новые переменные в ecs.tf
:
locals {
container_vars = {
...
sqs_access_key = aws_iam_access_key.prod_sqs.id
sqs_secret_key = aws_iam_access_key.prod_sqs.secret
sqs_name = aws_sqs_queue.prod.name
}
}
и передайте их контейнерам в backend_container.json.tpl
:
[
{
...
"environment": [
...
{
"name": "AWS_REGION",
"value": "${region}"
},
{
"name": "CELERY_BROKER_URL",
"value": "sqs://${urlencode(sqs_access_key)}:${urlencode(sqs_secret_key)}@"
},
{
"name": "CELERY_TASK_DEFAULT_QUEUE",
"value": "${sqs_name}"
}
],
...
}
]
Итак, мы передали учетные данные SQS службам ECS. Затем добавьте следующее содержимое в ecs.tf
и запустите terraform apply
:
...
# Cloudwatch Logs
...
resource "aws_cloudwatch_log_stream" "prod_backend_worker" {
name = "prod-backend-worker"
log_group_name = aws_cloudwatch_log_group.prod_backend.name
}
resource "aws_cloudwatch_log_stream" "prod_backend_beat" {
name = "prod-backend-worker"
log_group_name = aws_cloudwatch_log_group.prod_backend.name
}
...
# Worker
resource "aws_ecs_task_definition" "prod_backend_worker" {
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
family = "backend-worker"
container_definitions = templatefile(
"templates/backend_container.json.tpl",
merge(
local.container_vars,
{
name = "prod-backend-worker"
command = ["celery", "-A", "django_aws", "worker", "--loglevel", "info"]
log_stream = aws_cloudwatch_log_stream.prod_backend_worker.name
},
)
)
depends_on = [aws_sqs_queue.prod, aws_db_instance.prod]
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.prod_backend_task.arn
}
resource "aws_ecs_service" "prod_backend_worker" {
name = "prod-backend-worker"
cluster = aws_ecs_cluster.prod.id
task_definition = aws_ecs_task_definition.prod_backend_worker.arn
desired_count = 2
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
launch_type = "FARGATE"
scheduling_strategy = "REPLICA"
enable_execute_command = true
network_configuration {
security_groups = [aws_security_group.prod_ecs_backend.id]
subnets = [aws_subnet.prod_private_1.id, aws_subnet.prod_private_2.id]
assign_public_ip = false
}
}
# Beat
resource "aws_ecs_task_definition" "prod_backend_beat" {
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 256
memory = 512
family = "backend-beat"
container_definitions = templatefile(
"templates/backend_container.json.tpl",
merge(
local.container_vars,
{
name = "prod-backend-beat"
command = ["celery", "-A", "django_aws", "beat", "--loglevel", "info"]
log_stream = aws_cloudwatch_log_stream.prod_backend_beat.name
},
)
)
depends_on = [aws_sqs_queue.prod, aws_db_instance.prod]
execution_role_arn = aws_iam_role.ecs_task_execution.arn
task_role_arn = aws_iam_role.prod_backend_task.arn
}
resource "aws_ecs_service" "prod_backend_beat" {
name = "prod-backend-beat"
cluster = aws_ecs_cluster.prod.id
task_definition = aws_ecs_task_definition.prod_backend_beat.arn
desired_count = 1
deployment_minimum_healthy_percent = 50
deployment_maximum_percent = 200
launch_type = "FARGATE"
scheduling_strategy = "REPLICA"
enable_execute_command = true
network_configuration {
security_groups = [aws_security_group.prod_ecs_backend.id]
subnets = [aws_subnet.prod_private_1.id, aws_subnet.prod_private_2.id]
assign_public_ip = false
}
}
Здесь мы создали:
- Потоки журналов Cloudwatch для рабочего и рабочего.
- Определение задачи ECS рабочего и службу ECS. Мы указали
desired_count=2
, чтобы показать, как несколько рабочих могут работать для одной очереди. В будущем мы будем масштабировать рабочие ECS в зависимости от загрузки процессора. - Определение задачи ECS и службы ECS. Здесь мы указали
desired_count=1
, потому что мы не хотим планировать дубликаты для периодических задач.
Давайте проверим наши службы в консоли ECS.
Вот наши службы worker и beat:
Вот задания worker и beat. Видно, что ECS создает две задачи для службы worker и только одну задачу для службы beat:
Вот журналы рабочего. Пока что мы видим в журналах только задачи beat:
Давайте добавим новую задачу из Интернета. Нажмите https://api.example53.xyz/create-task URL (замените домен на свой). В ответ вы должны увидеть сообщение ‘Task added’. Затем вернитесь к рабочим журналам ECS и выберите интервал ’30s’, чтобы увидеть последние события журнала. В журналах вы должны увидеть сообщения ‘Starting web task’ и ‘Done web task’.
Итак, мы успешно запустили ECS для рабочего и beat процессов и убедились, что и web, и beat задачи Celery выполняются успешно.
Мы закончили с репозиторием infrastructure
, так что вы можете зафиксировать и распространить изменения.
Обновление развертывания
Осталась еще одна задача. Чтобы гарантировать, что мы будем обновлять наши службы ECS при каждом развертывании, нам нужно изменить наш ./scripts/deploy.sh
. Добавим ту же инструкцию, что и для службы web
:
...
echo "Updating web..."
aws ecs update-service --cluster prod --service prod-backend-web --force-new-deployment --query "service.serviceName" --output json
echo "Updating worker..."
aws ecs update-service --cluster prod --service prod-backend-worker --force-new-deployment --query "service.serviceName" --output json
echo "Updating beat..."
aws ecs update-service --cluster prod --service prod-backend-beat --force-new-deployment --query "service.serviceName" --output json
echo "Done!"
Таким образом, при каждом push мы будем принудительно создавать новое развертывание для служб worker и beat на ECS.
Зафиксируйте и проталкивайте изменения. Дождитесь CI/CD и проверьте сервисы в ECS Console. Через некоторое время появятся новые задачи:
Вы можете запланировать больше веб-задач и посмотреть их в журналах, чтобы убедиться, что все работает так, как ожидалось.
Конец
Поздравляем! Мы успешно создали экземпляр AWS SQS и добавили Celery worker + beat сервисы в ECS. Наше Django-приложение может запускать долгоиграющие задачи в фоновом рабочем процессе.
Вы можете найти исходный код проектов бэкенда и инфраструктуры здесь и здесь.
Если вам нужен технический консалтинг по вашему проекту, загляните на наш сайт или свяжитесь со мной напрямую на LinkedIn.