Развертывание приложения Django на AWS с помощью Terraform. Настройка Celery и SQS

Это пятая часть руководства «Развертывание приложения 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.

  1. Нажмите «Запустить экземпляр».

  1. Выберите любое имя для вашего экземпляра. Я выберу Test Server.

  1. Прокрутите вниз до карточки «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.

Оцените статью
devanswers.ru
Добавить комментарий