Localstack SNS-топик не передает тело сообщения на подписанный HTTP-эндпоинт

Вопрос или проблема

Проблема

Тема SNS, работающая в контейнере LocalStack, отправляет уведомление на подписанный HTTP конечный пункт, работающий в отдельном контейнере, но API получает {} вместо сообщения, отправленного в SNS.

Шаги для воспроизведения

Для краткости я исключил Dockerfile и код приложения my-api. Вы можете использовать любой веб-сервер (FastAPI, Express и т.д.), лишь бы он прослушивал порт 3000 и имел конечный пункт POST /events. В этом API сейчас ничего особенного не происходит, он просто регистрирует полученное событие.

  1. Создайте следующий файл docker-compose.yml
version: '3.8'

services:
  my-api:
    build:
      context: .
    ports:
      - 3000:3000
    volumes:
      - ./src:/opt/my-api/src

  localstack:
    image: localstack/localstack
    container_name: localstack
    ports:
      - 4566:4566
    environment:
      - SERVICES=sns
      - AWS_DEFAULT_REGION=us-east-1
    volumes:
      - localstack-data:/var/lib/localstack
      # https://docs.localstack.cloud/references/init-hooks/
      - ./localstack-init-scripts/:/etc/localstack/init/ready.d/

volumes:
  localstack-data:

  1. Создайте Python-скрипт localstack-init-scripts/init.py для инициализации контейнера LocalStack.
import boto3
import requests

# Клиент SNS
sns = boto3.client('sns', endpoint_url="http://localhost:4566", region_name="us-east-1")

# Создание темы
topic_arn = sns.create_topic(Name="local-events")['TopicArn']
print('Topic Arn:', topic_arn)

# Подписка HTTP конечного пункта на тему
subscription_arn = sns.subscribe(
    TopicArn=topic_arn,
    Protocol="http",
    Endpoint="http://host.docker.internal:3000/events",
    # Я также пробовал Endpoint="http://my-api:3000/events",
    ReturnSubscriptionArn=True
)['SubscriptionArn']
print('Subscription Arn:', subscription_arn)

# Получение токена подписки и подтверждение подписки
token_res = requests.get(f'http://localhost:4566/_aws/sns/subscription-tokens/{subscription_arn}').json()
print('Token Res:', token_res)
confirmation_status_code = sns.confirm_subscription(
    TopicArn=topic_arn,
    Token=token_res['subscription_token']
).get('ResponseMetadata', {}).get('HTTPStatusCode', 500)

confirmed = True if confirmation_status_code == 200 else False 
print('Subscription Confirmed:', confirmed)

  1. Запустите Docker-сервисы
docker compose up
  1. Отправьте тестовое уведомление на тему local-events
aws --endpoint-url http://localhost:4566 sns publish \
  --topic-arn arn:aws:sns:us-east-1:000000000000:local-events \
  --message '{ "msg": "Это тестовое сообщение" }'

Результаты

После выполнения указанных выше команд вы должны увидеть логи docker, похожие на

localstack                    | 2024-10-01T01:38:29.993  INFO --- [et.reactor-0] localstack.request.aws     : AWS sns.CreateTopic => 200
localstack                    | Topic Arn: arn:aws:sns:us-east-1:000000000000:local-events
localstack                    | 2024-10-01T01:38:29.997  INFO --- [et.reactor-0] localstack.request.aws     : AWS sns.Subscribe => 200
localstack                    | Subscription Arn: arn:aws:sns:us-east-1:000000000000:local-events:e98cb2ad-565c-4160-a77c-6a4bdd21350f
localstack                    | 2024-10-01T01:38:30.001  INFO --- [et.reactor-0] localstack.request.http    : GET /_aws/sns/subscription-tokens/arn:aws:sns:us-east-1:000000000000:local-events:e98cb2ad-565c-4160-a77c-6a4bdd21350f => 200
localstack                    | Token Res: {'subscription_token': '75732d656173742d312f8c8e1e8b8c8e1e8b8c8e1e8b8c8e1e8b8c8e1e8b8c8e', 'subscription_arn': 'arn:aws:sns:us-east-1:000000000000:local-events:e98cb2ad-565c-4160-a77c-6a4bdd21350f'}
localstack                    | 2024-10-01T01:38:30.004  INFO --- [et.reactor-0] localstack.request.aws     : AWS sns.ConfirmSubscription => 200
localstack                    | Subscription Confirmed: True
localstack                    | Ready.
my-api-1                      | Получено событие {}

Шаги по устранению неполадок ~~на данный момент~~

  1. Я зашел в контейнер Localstack и выполнил curl -XPOST http://host.docker.internal:3000/events -d '{ "msg": "сообщение теста curl" }', получил ожидаемый ответ и увидел следующее в логах docker
my-api-1                      | Получено событие { '{ "msg": "сообщение теста curl" }': '' }
  1. Подтвердил, что тема была подписана, выполнив awslocal sns get-subscription-attributes --subscription-arn "<subscription-arn-from-logs>" и получил ответ с "ConfirmationWasAuthenticated": "true" и "PendingConfirmation": "false"

Ссылки

У меня уже была эта проблема.

Похоже, проблема связана с тем, как LocalStack обрабатывает формат сообщений SNS при отправке на HTTP конечный пункт. Вот несколько вещей, которые можно попробовать:

  1. Формат сообщения:
    SNS обычно оборачивает сообщение в JSON-структуру. Попробуйте изменить ваш API, чтобы он ожидал этот формат:

    @app.post("/events")
    async def receive_event(event: dict):
        message = event.get('Message', '{}')
        print(f"Получено сообщение: {message}")
        # Парсинг сообщения, если это JSON
        try:
            message_json = json.loads(message)
            print(f"Парсированное сообщение: {message_json}")
        except json.JSONDecodeError:
            print("Сообщение не является JSON")
    
  2. Доставка сырого сообщения:
    Включите доставку сырого сообщения для подписки. Добавьте это в ваш init.py:

    sns.set_subscription_attributes(
        SubscriptionArn=subscription_arn,
        AttributeName="RawMessageDelivery",
        AttributeValue="true"
    )
    
  3. Сетевая конфигурация:
    Убедитесь, что ваши контейнеры могут общаться. Попробуйте использовать имя службы вместо host.docker.internal:

    Endpoint="http://my-api:3000/events"
    

    Возможно, вам потребуется добавить сеть в ваш docker-compose.yml:

    services:
      my-api:
        # ... другая конфигурация ...
        networks:
          - mynetwork
      localstack:
        # ... другая конфигурация ...
        networks:
          - mynetwork
    
    networks:
      mynetwork:
    
  4. Отладка:
    Добавьте больше логирования в ваш API, чтобы увидеть полный запрос:

    @app.post("/events")
    async def receive_event(request: Request):
        body = await request.body()
        headers = request.headers
        print(f"Получены заголовки: {headers}")
        print(f"Получено тело: {body}")
    
  5. Версия LocalStack:
    Убедитесь, что вы используете последнюю версию LocalStack. В более старых версиях могут быть ошибки.

  6. Структура сообщения:
    При публикации попытайтесь включить параметр MessageStructure:

    aws --endpoint-url http://localhost:4566 sns publish \
      --topic-arn arn:aws:sns:us-east-1:000000000000:local-events \
      --message '{"default": "{ \"msg\": \"Это тестовое сообщение\" }"}' \
      --message-structure json
    

Если ни одно из этих решений не устраняет проблему, возможно, это ошибка в реализации SNS в LocalStack. В этом случае вы можете рассмотреть возможность открытия проблемы в репозитории LocalStack на GitHub репозиторий.

Ответ или решение

Проблема, с которой вы столкнулись, заключается в том, что LocalStack не передает тело сообщения в SNS Topic на подписанный HTTP-эндпоинт, и вы получаете пустой объект {} вместо ожидаемого сообщения. Рассмотрим несколько решений для этой проблемы и шаги, которые вы можете предпринять, чтобы исправить ситуацию.

Шаги для решения проблемы

  1. Формат сообщения: SNS обычно оборачивает сообщение в JSON-структуру. Измените ваш API, чтобы он ожидал этот формат сообщения. Пример кода для вашего FastAPI или Express приложения:

    @app.post("/events")
    async def receive_event(event: dict):
       message = event.get('Message', '{}')
       print(f"Received message: {message}")
       # Пытаемся распарсить сообщение, если это JSON
       try:
           message_json = json.loads(message)
           print(f"Parsed message: {message_json}")
       except json.JSONDecodeError:
           print("Message is not JSON")
  2. Сырой формат сообщения: Включите сырую доставку сообщений для подписки. Добавьте следующий код в ваш init.py для конфигурации подписки:

    sns.set_subscription_attributes(
       SubscriptionArn=subscription_arn,
       AttributeName="RawMessageDelivery",
       AttributeValue="true"
    )
  3. Сетевая конфигурация: Убедитесь, что ваши контейнеры могут связываться друг с другом. Попробуйте использовать имя сервиса вместо host.docker.internal в Endpoint. Пример:

    Endpoint="http://my-api:3000/events"

    Также рекомендуется добавить пользовательскую сеть к вашему docker-compose.yml:

    services:
     my-api:
       # ... другая конфигурация ...
       networks:
         - mynetwork
     localstack:
       # ... другая конфигурация ...
       networks:
         - mynetwork
    
    networks:
     mynetwork:
  4. Отладка: Добавьте больше логирования в ваш API, чтобы видеть полный запрос и его содержимое:

    @app.post("/events")
    async def receive_event(request: Request):
       body = await request.body()
       headers = request.headers
       print(f"Received headers: {headers}")
       print(f"Received body: {body.decode('utf-8')}")
  5. Версия LocalStack: Проверьте, используете ли вы последнюю версию LocalStack. В более ранних версиях могли быть ошибки, которые уже исправлены в новых релизах.

  6. Структура сообщения: При публикации сообщения попробуйте использовать параметр MessageStructure. Пример команды для публикации:

    aws --endpoint-url http://localhost:4566 sns publish \
     --topic-arn arn:aws:sns:us-east-1:000000000000:local-events \
     --message '{"default": "{ \"msg\": \"This is a Test Message\" }"}' \
     --message-structure json

Заключение

Если ни одно из предложенных решений не позволяет вам решить проблему, возможно, это баг в реализации SNS в LocalStack. В таком случае вы можете рассмотреть возможность открытия запроса на исправление на GitHub репозитории LocalStack.

Следуя этим шагам, вы сможете устранить проблемы с передачей сообщения от SNS Topic к вашему HTTP-эндпоинту. Удачи!

Оцените материал
Добавить комментарий

Капча загружается...