Предопределенный URL S3 работает через 90 минут после создания корзины - PullRequest
1 голос
/ 09 июня 2019

Мы генерируем предварительно заданные URL-адреса, чтобы пользователи могли загружать файлы непосредственно в сегменты S3.Выполняя интеграционные тесты, мы обнаружили неудачный тест, в котором запрос HTTP PUT на предварительно назначенный URL-адрес дал ответ SignatureDoesNotMatch об ошибке.Удивительно, но тот же код работал нормально, используя другое ведро.Мы продолжали примерять исходное ведро, которое привело к сбою теста, и были удивлены, когда оно неожиданно начало работать без каких-либо реальных изменений кода.

Мы заметили, что прошло примерно два часа после того, как мы создали ведро, когдатест успешно пройден.Поскольку мы находимся по UTC + 0200, мы подозревали, что проблема как-то связана с этой разницей во времени и / или с какой-то проблемой синхронизации часов.Мы намереваемся подтвердить наши подозрения, что тот же предварительно назначенный URL-адрес неожиданно заработает только после того, как пройдет достаточно времени.SPOILER: Это делает!

Следующий код создает совершенно новую корзину, генерирует предварительно назначенный URL-адрес, подходящий для загрузки файла (ClientMethod='put_object'), и пытается HTTP PUT некоторые данные, используя requests библиотека.Мы повторяем попытку PUTting данных каждые 60 секунд до тех пор, пока они не завершатся успешно через 5419 секунд (или 90 минут) после создания корзины.

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

import logging
import time

import boto3
import requests

from botocore.client import Config

logger = logging.getLogger(__name__)

# region = "eu-central-1"
# region = "eu-west-1"
# region = "us-west-1"
region = "us-east-1"
s3_client = boto3.client('s3', region_name=region, config=Config(signature_version='s3v4'))


if __name__ == "__main__":
    bucket_name = "some-globally-unique-bucket-name"

    key_for_file = "test-file.txt"

    # create bucket
    if region == "us-east-1":
        # https://github.com/boto/boto3/issues/125
        s3_client.create_bucket(Bucket=bucket_name, ACL='private')
    else:
        s3_client.create_bucket(Bucket=bucket_name, ACL='private',
                                CreateBucketConfiguration={'LocationConstraint': region})
    creation_time = time.time()

    # generate presigned URL
    file_data = b"Hello Test World"
    expires_in = 4 * 3600
    url = s3_client.generate_presigned_url(ClientMethod='put_object', ExpiresIn=expires_in,
                                           Params={'Bucket': bucket_name, 'Key': key_for_file})

    time_since_bucket_creation = time.time() - creation_time
    time_interval = 60
    max_time_passed = expires_in
    success = False
    try:
        while time_since_bucket_creation < max_time_passed:
            response = requests.put(url, data=file_data)
            if response.status_code == 200:
                success = True
                break

            if b"<Code>SignatureDoesNotMatch</Code>" in response.content:
                reason = "SignatureDoesNotMatch"
            else:
                reason = str(response.content)

            time_since_bucket_creation = time.time() - creation_time
            print("="*50)
            print(f"{time_since_bucket_creation:.2f} s after bucket creation")
            print(f"unable to PUT data to url: {url}")
            print(f"reason: {reason}")
            print(response.content)
            time.sleep(time_interval)
    except KeyboardInterrupt:
        print("Gracefully shutting down...")

    if success:
        print("YAY! File Upload was successful!")
        time_since_bucket_creation = time.time() - creation_time
        print(f"{time_since_bucket_creation:.2f} seconds after bucket creation")
        s3_client.delete_object(Bucket=bucket_name, Key=key_for_file)

    # delete bucket
    s3_client.delete_bucket(Bucket=bucket_name)

Мы запускаем интеграционные тесты с кластером AWS EKS, где мы создаем кластер вместе с некоторымибазы данных, корзины S3 и т. д. и все разрушают после завершения испытаний.Необходимость ждать 90 минут для назначения URL-адресов для работы невозможна.

Мои вопросы
Я что-то не так делаю?
Это ожидаемое поведение?Есть ли приемлемый обходной путь?
Может ли кто-нибудь, пожалуйста, подтвердить это поведение, используя приведенный выше код?

РЕДАКТИРОВАТЬ
Я обновил код, чтобы создать корзину в "нас"-east-1 "регион, как предложено" Michael - sqlbot "в комментариях.Странный оператор if необходим, как задокументировано здесь .Я могу подтвердить подозрение Майкла о том, что поведение НЕ воспроизводится с помощью «us-east-1».

В случае, если это представляет интерес, возвращаемый XML в случае ошибки:

<?xml version="1.0" encoding="UTF-8"?>
<Error>
    <Code>SignatureDoesNotMatch</Code>
    <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
    <AWSAccessKeyId>REDACTED</AWSAccessKeyId>
    <StringToSign>AWS4-HMAC-SHA256
    20190609T170351Z
    20190609/eu-central-1/s3/aws4_request
    c143cb44fa45c56e52b04e61b777ae2206e0aaeed40dafc78e036878fa91dfd6</StringToSign>
    <SignatureProvided>REDACTED</SignatureProvided>
    <StringToSignBytes>REDACTED</StringToSignBytes>
    <CanonicalRequest>PUT
    /test-file.txt
    X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=REDACTED%2F20190609%2Feu-central-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20190609T170351Z&amp;X-Amz-Expires=14400&amp;X-Amz-SignedHeaders=host
    host:some-globally-unique-bucket-name.s3.eu-central-1.amazonaws.com

    host
    UNSIGNED-PAYLOAD</CanonicalRequest>
    <CanonicalRequestBytes>REDACTED</CanonicalRequestBytes>
    <RequestId>E6CBBC7D2E4D322E</RequestId>
    <HostId>j1dM1MNaXaDhzMUXKhqdHd6+/Rl1C3GzdL9YDq0CuP8brQZQV6vbyE9Z63HBHiBWSo+hb6zHKVs=</HostId>
</Error>

1 Ответ

1 голос
/ 12 июня 2019

Вот что вы наталкиваетесь:

Временное перенаправление - это тип ответа об ошибке, который сигнализирует запрашивающей стороне, что он должен отправить запрос в другую конечную точку.Из-за распределенной природы Amazon S3 запросы могут быть временно перенаправлены на неправильный объект.Скорее всего, это произойдет сразу после создания или удаления сегментов.

Например, если вы создаете новый блок и сразу же делаете запрос в блок, вы можете получить временное перенаправление в зависимости от ограничения местоположения.из ведра.Если вы создали корзину в регионе AWS Восток США (Северная Вирджиния), вы не увидите перенаправления, поскольку это также конечная точка Amazon S3 по умолчанию.

Однако, если корзина создается в любом другом регионелюбые запросы к сегменту отправляются в конечную точку по умолчанию, пока распространяется запись DNS сегмента.Конечная точка по умолчанию перенаправляет запрос на правильную конечную точку с ответом HTTP 302.Временные перенаправления содержат URI для правильного средства, которое можно использовать для немедленной повторной отправки запроса.

https://docs.aws.amazon.com/AmazonS3/latest/dev/Redirects.html

Обратите внимание, что последняя часть - , которую выМожно использовать для немедленной повторной отправки запроса - не совсем точно.Вы можете - но если в запросе используется подпись версии 4, то после перенаправления на новое имя хоста возникнет ошибка SignatureDoesNotMatch, поскольку имя хоста будет другим.В старые времена Signature Version 2 имя сегмента было включено в сигнатуру, но само имя хоста конечной точки не было, поэтому перенаправление на другое имя хоста конечной точки не сделало бы недействительной подпись.

Ничто из этого не будетбыть проблемой, если boto делает правильные вещи и использует правильную региональную конечную точку для создания подписанного URL-адреса - но по какой-то причине он использует «глобальную» (универсальную) конечную точку - что заставляет S3 выпустить эти перенаправления для первогонесколько минут времени существования корзины, потому что DNS не был обновлен, поэтому запрос неверно перенаправляется на us-east-1 и перенаправляется.Вот почему я подозревал, что us-east-1 не будет демонстрировать такое поведение.

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

В качестве обходного пути,Конструктор клиента принимает аргумент endpoint_url, который, кажется, служит цели.Как выясняется, s3.${region}.amazonaws.com является действительной конечной точкой для каждой области S3, поэтому они могут быть построены из строки региона.

s3_client = boto3.client('s3', region_name=region, endpoint_url=('https://s3.' + region + '.amazonaws.com'), config=...)

Долгосрочными пользователями S3 могут бытьс подозрением относится к утверждению, что все регионы поддерживают это, но это точно на момент написания этой статьи.Первоначально некоторые регионы ранее использовали тире, а не точку, например, s3-us-west-2.amazonaws.com, и это все еще действует в тех более старых регионах, но теперь все регионы поддерживают каноническую форму, упомянутую выше.

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