[PYTHON] Émettre une URL signée avec AWS SQS

AWS S3 a une "URL signée". SQS fait quelque chose de similaire à cette fonctionnalité.

Quand l'utilisez vous?

Par exemple, supposons que vous souhaitiez créer un système comme celui-ci.

device_image.png

Considérez la configuration sur AWS.

** <Configuration A: API Gateway + Lambda> **

Lambda + API Gateway a la réputation d'être bon marché, mais elle peut être assez coûteuse pour traiter un grand nombre de requêtes.

talk_image_gateway.png

C'est un calcul approximatif, mais lorsqu'il y a 172,8 millions de demandes par jour, les frais d'accumulation de données sont les suivants.

un service Prix par million de demandes Charge journalière conditions
Lambda Dépend des conditions 387 USD Traitement moyen d'une seconde, allocation de mémoire 128 Mo
Api Gateway 4.25 USD 731 USD REST API
SQS 0.4 USD 69 USD

Total: 1187 $ par jour

Avec les retards Lambda, WAF, CloudFront, l'authentification et plus encore, cela va encore plus haut.

** <Configuration B: AWS IoT + Rule Engine + SQS> **

Le prix sera inférieur si la configuration consiste à transférer d'AWS IoT vers SQS avec le moteur de règles. Mais cette fois ...

un service Prix par million de demandes Charge journalière conditions
AWS IoT Envoyer: 1.2 USD
Règle: 0.18 USD
Action: 0.18 USD
268 USD règle+Passer à l'action
SQS 0.4 USD 69 USD

Total: 337 $ par jour

talk_image_iot.png

Cette fois, MQTT est impossible en raison du port, et WebSocket est également impossible en raison du côté terminal.

** **

D'ailleurs, si vous pouvez envoyer directement du terminal à SQS, cela vous coûtera environ 1/20, 69 dollars par jour. Il y a un problème de sécurité car l'AWS-SDK ne peut pas être utilisé. Quelque chose de plus que ça ...

talk_image_sqs.png

C'est probablement Sorya.

** <Configuration D: URL signée SQS +> **

Si vous y réfléchissez bien, il semble que vous puissiez bien le faire si vous pouvez l'envoyer directement à SQS avec une authentification qui n'utilise pas le SDK.

Afin de créer un tel mécanisme, nous réaliserons une URL signée SQS +.

Dessin d'achèvement

draw.png

** Le traitement à exécuter et le calendrier d'exécution de chacun sont les suivants **

En traitement Cible Horaire
Obtenir l'URL signée SQS Terminal → Passerelle API Exécuter une fois toutes les 15 minutes
Envoyer l'état du terminal à SQS Terminal → SQS Exécuter une fois toutes les 5 secondes

** Les données lancées par SQS sont supposées être les suivantes cette fois **

Données à envoyer Contenu des données
Nom de la file d'attente SQS sqs-send-request-test-0424
Schéma des données à transmettre Open/Ouvert (terminal activé, sentiment humain activé)
Close/Ouvert (terminal éteint, sentiment humain activé)
Open/Fermer (terminal activé, sensation humaine désactivée)
Close/Fermer (terminal éteint, sensation humaine désactivée)
* Envoyez l'une des 4 façons

Obtenez l'URL signée SQS (exécutée une fois toutes les 15 minutes)

Demander une URL signée à API Gateway [^ 1]

[^ 1]: * L'authentification API Gateway est omise cette fois. Si vous souhaitez vous authentifier, veuillez mettre les informations d'authentification dans l'en-tête des données transmises.

Demande côté terminal


curl "https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/url" -s -X POST \
-d '{"que_name":"sqs-send-request-test-0424", "patterns":["Open/Open","Close/Open","Open/Close","Close/Close"]}'

Si la demande aboutit, vous recevrez le même nombre d'URL signées que le modèle de données.

Données reçues côté terminal


{
    "url": {
        "Open/Open": "https://sqs.ap-northeast-1.amazonaws.com/xxxxxx/sqs-send-request-test-0424?AWSAccessKeyId=xxxxx&Action=SendMessage&MessageBody=Open%2FOpen&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-04-30T10%3A42%3A54&Version=2012-11-05&Signature=xxxxxxxxxxxxxxxxxxxxx", 
        "Close/Open": "https://sqs.ap-northeast-1.amazonaws.com/xxxxxx/sqs-send-request-test-0424?AWSAccessKeyId=xxxxx&Action=SendMessage&MessageBody=Close%2FOpen&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-04-30T10%3A42%3A54&Version=2012-11-05&Signature=xxxxxxxxxxxxxxxxxxxxx",
        "Open/Close": "https://sqs.ap-northeast-1.amazonaws.com/xxxxxx/sqs-send-request-test-0424?AWSAccessKeyId=xxxxxx&Action=SendMessage&MessageBody=Open%2FClose&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-04-30T10%3A42%3A54&Version=2012-11-05&Signature=xxxxxxxxxxxxxxxxxxxxx", 
        "Close/Close": "https://sqs.ap-northeast-1.amazonaws.com/xxxxxx/sqs-send-request-test-0424?AWSAccessKeyId=xxxxxx&Action=SendMessage&MessageBody=Close%2FClose&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-04-30T10%3A42%3A54&Version=2012-11-05&Signature=xxxxxxxxxxxxxxxxxxxxx"
    }
}

Envoyer l'état du terminal à SQS (exécuter une fois toutes les 5 secondes)

L'URL à envoyer correspond aux données des données ["url"] [$ {state you want to send}]. Dans l'exemple, "Ouvrir / Fermer" est envoyé.

Demande côté terminal


curl "https://sqs.ap-northeast-1.amazonaws.com/xxxxxx/sqs-send-request-test-0424?AWSAccessKeyId=xxxxxx&Action=SendMessage&MessageBody=Open%2FClose&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2020-04-30T10%3A42%3A54&Version=2012-11-05&Signature=xxxxxxxxxxxxxxxxxxxxx"

Chaque fois que vous l'exécutez, un message $ {state you want to send} sera enregistré dans le sujet défini. Dans l'exemple, "Ouvrir / Fermer" est enregistré dans "sqs-send-request-test-0424".

capture_2.png

la mise en oeuvre

En guise de préparation préliminaire, faites une file d'attente avec SQS. Entrez le nom de la file d'attente que vous aimez et cliquez sur "File d'attente rapide" en bas de l'écran.

capture_5.png

Créez également un utilisateur IAM dédié à SQS. Définissez AmazonSQSFullAccess sur la stratégie. Aucun privilège autre que SQS n'est requis.

capture_3.png

Après avoir créé l'utilisateur IAM, obtenez l'ID de la clé d'accès et la clé d'accès secrète.

Mettre en œuvre Lambda

Créez Lambda avec Python 3.8.

Définissez les variables d'environnement Lambda comme suit.

Clé de variable d'environnement Valeur à définir
AWS_ACCOUNT_NUMBER Numéro d'identification de compte AWS
(Le numéro à 12 chiffres en bas à gauche de l'écran IAM)
SQS_ACCOUNT_ID ID de clé d'accès utilisateur IAM dédié à SQS
SQS_SECRET_KEY Clé d'accès secrète pour les utilisateurs IAM dédiée à SQS

Collez la source suivante dans Lambda.

Lambda



# coding: utf-8
import boto3, json, hashlib, hmac, base64, os 
from datetime import datetime, timezone, timedelta
from urllib import parse as url_encode

#Informations de signature (format SHA256, la version de signature est 2)
SIGNATURE_METHOD = "HmacSHA256"
SIGNATURE_VERSION = "2"
ENCODE = "utf-8"

#Informations sur la méthode SQS (envoyer un message à la file d'attente à l'aide de l'API REST GET)
HTTP_METHOD = "GET"
SQS_METHOD = "SendMessage"
AWS_VERSION = "2012-11-05"

#Fuseau horaire
UTC_TIMEZONE = timezone(timedelta(hours = 0), 'UTC')

class Credentials:
    """
Définir les informations d'identification de l'utilisateur IAM
    """
    @staticmethod
    def from_iam():
        instance = Credentials()
        instance.aws_access_key = os.environ.get("SQS_ACCOUNT_ID", DEFAULT.SQS_ACCOUNT_ID)
        instance.aws_secret_key = os.environ.get("SQS_SECRET_KEY", DEFAULT.SQS_SECRET_KEY)
        return instance

class Endpoint:
    """
Définir les informations du point de terminaison SQS
    """
    def __init__(self, topic_name):
        self.protocol = "https"
        self.host_name = "sqs.{}.amazonaws.com".format(os.environ.get("AWS_REGION", DEFAULT.AWS_REGION))
        self.url_path = "/{}/{}".format(os.environ.get("AWS_ACCOUNT_NUMBER", DEFAULT.AWS_ACCOUNT_NUMBER), topic_name)

    @property    
    def url(self):
        return f"{self.protocol}://{self.host_name}{self.url_path}"

#Créer une URL signée SQS
def create_presigned_url(credential, endpoint, message):

    #Obtenez la date et l'heure actuelles avec UTC et convertissez-les en chaîne de caractères
    current_date = datetime.now().astimezone(UTC_TIMEZONE)
    current_date_str = url_encode.quote(
        current_date.strftime('%Y-%m-%dT%H:%M:%S')
    )

    #Créer un hachage avec une clé d'accès secrète en fonction des données à envoyer
    return endpoint.url + "?" + create_query(SQS_METHOD, message, credential.aws_access_key, current_date_str, option = {
        #Étant donné que les données de hachage sont un tableau d'octets, l'encodage d'URL + l'encodage en base64 afin qu'il puisse être envoyé par GET.
        "Signature" : url_encode.quote(
            base64.b64encode(
                sign(
                    #Spécifiez la clé d'accès secrète à utiliser pour la signature
                    credential.aws_secret_key.encode(ENCODE), 
                    #Spécifiez les données de transmission (les données spécifiées sont hachées avec SHA256)
                    create_certificate(endpoint, SQS_METHOD, message, credential.aws_access_key, current_date_str)
                )
            )
        )
    })

#Hash avec clé d'accès secrète
def sign(key, msg):
    return hmac.new(key, msg.encode(ENCODE), hashlib.sha256).digest()

#Créer des données de signature au format v2
#Se référer au document officiel pour le format (https)://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html)
def create_certificate(endpoint, method, message_body, aws_access_key, current_date_str):
    return "\n".join([
        HTTP_METHOD,
        endpoint.host_name,
        endpoint.url_path,
        create_query(method, message_body, aws_access_key, current_date_str)
    ])

#Normaliser les données de requête
#Se référer au document officiel pour le format (https)://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html)
def create_query(method, message_body, aws_access_key, current_date_str, option = None):
    query_map = {
        "AWSAccessKeyId" : aws_access_key,
        "Action" : method,
        "MessageBody" : message_body,
        "SignatureMethod" : SIGNATURE_METHOD,
        "SignatureVersion" : SIGNATURE_VERSION,
        "Timestamp" : current_date_str,
        "Version" : AWS_VERSION
    }

    #La signature ne doit pas être incluse dans le hachage, uniquement lors de l'envoi
    #Ajouter des données à la requête si Signature est spécifiée
    if option is not None:
        query_map.update(option)

    #Convertir au format de requête GET
    return "&".join([
        f"{key}={value}"
        for key, value in query_map.items()
    ])

#Créez autant d'URL signées que vous le souhaitez
def request(credential, endpoint, data_patterns):
    url = {}
    for pattern in data_patterns:
        url[pattern] = create_presigned_url(credential, endpoint, url_encode.quote(pattern, safe = ""))
    return {
        "url" : url
    }

#Obtenez les données POSTées à partir des arguments APIGateway (HTTP API) (les données sont encodées en base64 du côté APIGateway)
def get_payload_from_event(event):
    payload_str = ""
    if event["isBase64Encoded"]:
        payload_str = base64.b64decode(event["body"].encode(ENCODE))
    else:
        payload_str = event["body"]
    return json.loads(payload_str)

#Point d'entrée lors de l'exécution de Lambda
def lambda_handler(event, context):
    payload = get_payload_from_event(event)
    return {
        'statusCode': 200,
        'body': json.dumps(request(Credentials.from_iam(), Endpoint(payload["que_name"]), payload["patterns"]))
    }

# ---------------------------------------------
#Ce qui suit est pour une exécution locale
#Spécifier lors de l'exécution sur autre que Lambda
# ---------------------------------------------

#Variables pour l'exécution locale
class _Default:
    def __init__(self):
        self.SQS_ACCOUNT_ID = "" #Spécifiez l'ID de clé d'accès IAM
        self.SQS_SECRET_KEY = "" #Spécifiez la clé secrète IAM
        self.AWS_ACCOUNT_NUMBER = "" #Spécifiez l'ID de compte AWS
        self.AWS_REGION = "ap-northeast-1" #Spécifiez la région où se trouve SQS
        self.SQS_QUEUE_NAME = "" #Spécifiez le nom de la file d'attente de destination
DEFAULT = _Default()

#Point d'entrée lors de l'exécution du débogage de l'environnement local
if __name__ == "__main__":
    print(json.dumps(request(Credentials.from_iam(), Endpoint(DEFAULT.SQS_QUEUE_NAME), [
        "Open/Open",  "Close/Open",  "Wait/Open",
        "Open/Close", "Close/Close", "Wait/Close",
        "Open/Wait",  "Close/Wait",  "Wait/Wait"
    ])))

Définissez le Lambda créé comme back-end d'API Gateway.

capture_6.png

Si vous déménagez, ce n'est pas grave si vous avez lu jusqu'ici. Après avoir déployé API Gateway, cela devrait fonctionner comme le "dessin terminé".

À partir de maintenant, ce sera une histoire détaillée.

Histoire détaillée

À l'origine, SQS dispose d'un mécanisme pour envoyer des messages par GET ou POST sans passer par AWS-SDK.

Référence: faire une requête d'API de requête https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-making-api-requests.html

Envoyer à la file d'attente publique en tant qu'utilisateur anonyme

La méthode de demande la plus simple est la suivante. Lorsque vous frappez l'URL sur le site de référence, un message (données) sera envoyé à la file d'attente (MyQueue). [^ 2]

[^ 2]: * Version est la version de SQS. Pour le moment, vous pouvez l'envoyer sans penser à rien.

https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue ? 
    Version = 2012-11-05 &
    Action = SendMessage &
    MessageBody = data

Notez que cette requête volera les messages d'utilisateurs anonymes. La file d'attente par défaut n'arrive pas avec une erreur d'autorisation.

Pour recevoir des messages dans cet état, vous devez les avoir configurés pour accepter la divulgation publique. Modifiez les autorisations de la file d'attente telles que capturées et cochez "Tout le monde".

capture_4.png

Envoyer vers une file d'attente qui n'autorise que des utilisateurs spécifiques

Si vous souhaitez envoyer à un utilisateur spécifique sans paramètre d'acceptation de la version publique, définissez «Qui suis-je».

https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue ?
    Version = 2012-11-05 &
    Action = SendMessage &
    MessageBody = data &
    AWSAccessKeyId = AKIA****************

Depuis que j'ai défini l'AWSAccessKeyID, "quelqu'un" peut être communiqué. Cependant, je ne sais pas si c'est vraiment la personne. Vous ne pouvez vous faire passer pour vous-même.

Utilisez la clé d'accès secrète pour prouver votre identité. Cependant, comme il n'est pas possible d'envoyer la clé d'accès secrète telle quelle, nous enverrons la version hachée.

https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue ?
    Version = 2012-11-05 &
    Action = SendMessage &
    MessageBody = data &
    AWSAccessKeyId = AKIA**************** &
    Signature = ********************************** &
    SignatureMethod = HmacSHA256 &
    SignatureVersion = 2 &
    Timestamp = 2020-04-30T10:42:54

La signature est un hachage des données à envoyer, en utilisant la clé d'accès secrète comme clé. Les données à envoyer sont l'ensemble de la requête à l'exclusion de la signature et des informations d'hôte combinées.

Signature Method est l'algorithme utilisé pour hacher Signature. Puisqu'il s'agit du HMAC-SHA256, je vais en parler à AWS.

La version de signature est la version de signature. Cette fois, j'utilise la version 2 de signature.

TimeStamp est la date et l'heure hachées.

Remarque: processus de signature de la version 2 de signature https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-2.html

Au fait, quelle est la différence par rapport à l'URL signée d'origine?

Le comportement est différent de celui de "l'URL signée" du S3 d'origine.

** La plage dans laquelle les données peuvent être modifiées après l'émission de l'URL est différente **

Dans SQS, la signature change en fonction du contenu du message envoyé. Avec S3, vous pouvez envoyer avec la même signature quel que soit le contenu du fichier que vous envoyez.

Pour envoyer différents messages avec SQS, vous devez émettre autant de signatures.

** La durée de vie des informations d'identification temporaires transmises par STS est différente **

Pour S3, l'ID de clé d'accès temporaire émis par Lambda sera disponible jusqu'à l'expiration de l'URL signée. Que se passe-t-il si vous émettez et accordez également un ID de clé d'accès temporaire et une clé d'accès secrète dans SQS?

Étant donné que l'ID de la clé d'accès disparaît à la fermeture de Lambda, l'authentification ne passera pas lorsque Lambda renvoie l'URL à API Gateway.

Recommended Posts

Émettre une URL signée avec AWS SQS
Générer une URL pré-signée avec golang
Générer une URL signée S3 avec boto
Créer un référentiel privé avec AWS CodeArtifact
J'ai essayé de créer un service de raccourcissement d'url sans serveur avec AWS CDK
Créez un environnement WardPress sur AWS avec Pulumi
Essayez Tensorflow avec une instance GPU sur AWS
Créez un système de synthèse bon marché avec des composants AWS
AWS Step Functions pour apprendre avec un exemple
Émettre l'URL signée Amazon CloudFront en Python
[Go] Diffusez du contenu privé avec l'URL signée CloudFront
[AWS] J'ai créé un BOT de rappel avec LINE WORKS
Émettre une URL limitée dans le temps S3 avec boto3 (avec confirmation de l'existence du fichier)
Créer une couche pour AWS Lambda Python dans Docker
Django + redis Publie une URL dédiée valable 1 heure
Format A4 avec python-pptx
[blackbird-sqs] Surveillance d'AWS SQS
Raccourcissement d'URL avec Python
Décorer avec un décorateur
Je viens de créer un environnement virtuel avec la couche AWS lambda
[AWS] J'ai créé un BOT de rappel avec LINE WORKS (implémentation)