Enmilocalfunciona

Thoughts, stories and ideas.

Jugando con AWS IoT Button, Secrets Manager y Spotify

Publicado por Sergio Lecuona el

AWS LambdaAmazon AWSAWS Secrets Manageriot

Antes de empezar a hablar de servicios Cloud y Amazon Web Services dejadme poneros un poco en…

Contexto

Hace unos meses fui padre de la mejor niña del universo conocido, Nerea, y como padres primerizos seguro hacemos cosas de las que cualquier padre experimentado se reiría. El caso es que mi mujer descubrió que la niña se quedaba dormida cuando le daba el pecho en nuestro dormitorio poniendo música de Ludovico Einaudi (os lo recomiendo).

alt

Así que, cada noche, a oscuras con la niña en brazos mi mujer pedía a nuestro Echo Dot de la mesilla: “Alexa, pon música de Ludovico Enaudi”. El problema es que pasado un rato y con la niña ya dormida tocaba parar la música, pero resulta que Alexa no se lleva bien con los susurros y tampoco podía gritarle “Alexa, para la música” porque la niña se despertaba. ¿Cómo lo solucionábamos? Gestos a la cámara y yo, desde el salón, le pedía a nuestro Amazon Echo Spot que la parara… Todo muy poco eficiente, la verdad.

Pero un día abrí un cajón y decubrí mi AWS IoT Button, cogiendo polvo ahí el pobre, e…

¡idea feliz!

alt

Para los que no lo conozcáis AWS IoT Button es un dispositivo basado en el hardware Amazon Dash Button, configurable vía wifi y que permite integraciones directas con los servicios de AWS, como por ejemplo, hacer de trigger para funciones Lambda.

Así que la idea feliz era utilizar mi botón para ejecutar una función Lambda que realizara llamadas a la API de Spotify pausando la lista de reproducción en curso, así de fácil.

alt

Lo primero antes de empezar es conocer un poco…

La API de Spotify

La verdad es que están muy bien documentadas e incluso tenéis una consola en la que podéis probarlas. https://developer.spotify.com/console/

La que me interesaba a mí es esta: https://developer.spotify.com/console/put-pause/

alt

Como veis es muy sencilla, la única parte interesante es obtener el OAuth access_token necesario para enviárselo en las cabeceras. Y para obtenerlo hay que entender el Flow de autenticación de Spotify:

alt

Resumiendo, hay que pasar por un login manual en el servicio de Spotify (punto 1). Esto nos devuelve un access_token (con tiempo de expiración) y refresh_token. Este segundo token es el interesante porque ya podremos utilizarlo desde nuestra lambda para ir obteniendo nuevos access_token válidos (punto 2) que nos permitan llamar a la API de Spotify que nos interesa (punto 3).

Por cierto, cuando hagáis la primera llamada a la API de Authorize (el punto 1), acordaos de pasar el parámetro scope con el valor user-modify-playback-state, porque si no vuestra lambda no tendrá permisos para actuar sobre la lista en ejecución.

En mi caso, esa primera llamada para obtener el access_token era:

https://accounts.spotify.com/authorize?client_id=********&response_type=code&redirect_uri=https%3A%2F%2F*******com%2Fcallback.html&scope=user-modify-playback-state  

Como veis, antes de nada tenía que dar de alta mi aplicación en el servicio de Spotify para obtener el client_id y secret_id, lo que podéis hacer desde el dashboard de la plataforma para developers: https://developer.spotify.com/dashboard/applications

alt

Llegados a este punto podéis observar que ya hay tres datos de Spotify que necesito guardar y tener disponibles cada vez que ejecute mi Lambda:

  • client_id
  • client secret
  • refresh_token

Y no es plan de tenerlo hardcodeado en mi función, ni tampoco dejarlos expuestos, así que aquí entra en juego uno de los servicios de AWS de los que quería hablaros…

AWS Secrets Manager

En mi opinión, AWS Secrets Manager es uno de los servicios más útiles (y más olvidados) de Amazon Web Services. Con este servicio podemos almacenar y recuperar secretos, rotar las claves (como por ejemplo las claves de administración de nuestras instancias RDS), y securizar vía policies el acceso a estos secretos.

En primer lugar, accedemos a nuestra cuenta de AWS y buscamos el servicio Secrets Manager donde podemos dar de alta nuestro primer secreto.

Tenemos las opciones de:

  • Credenciales para base de datos RDS
  • Credenciales para base de datos Redshift
  • Credenciales para base de datos DocumentDB
  • Credenciales para otra base de datos
  • Otro tipo de secretos (clave-valor).

En nuestro caso utilizaremos el último tipo pero, una de las grandes ventajas de este servicio es poder almacenar los secretos de nuestras instancias de bases de datos, automatizando la rotación:

alt

Como comentaba, en este escenario tenemos que almacenar un conjunto de claves-valor que agrupamos dentro de un mismo secreto, de esta manera podremos generar fácilmente IAM Policies específicas para nuestro proyecto que permitan el acceso a las credenciales de un proyecto específico.

alt

Una gran ayuda que aporta este servicio es que directamente nos da en diferentes lenguajes (Java, Javascript, C#, Python3, Ruby y Go), el código que tenemos que utilizar para recuperar nuestros secretos:

alt

Actualizando nuestro diagrama de la idea feliz, finalmente quedaría así:

alt

Hagamos un breve resumen:

  • Tenemos creada la aplicación en la plataforma de Spotify.
  • Hemos obtenido manualmente nuestro refresh_token llamando a la API de Authorize de Spotify.
  • Hemos almacenado en AWS Secrets Manager el client_id, client_secret y refresh_token.

Estamos listos para generar nuestra….

Función Lambda

No voy a hablar en profundidad de qué son las funciones Lambda ni qué son los entornos Serverless, o cómo gestionar bien el ciclo de vida de nuestras funciones. Me extendería demasiado y ya tenéis una serie de artículos geniales en este mismo blog: https://enmilocalfunciona.io/aprendiendo-serverless-framework-parte-1-introduccion/

En este ejemplo yo he desarrollado la función Lambda en Python, importando el módulo requests que no traen por defecto las lambdas y necesitamos para las llamadas a las APIs.
Os dejo el código completo de la función Lambda que, como veréis, se divide en:

  • Función para obtener claves desde el AWS Secrets Manager (directamente el código que me ha dado el servicio y que comentaba antes).
  • Función para obtener el OAuth access_token de Spotify
  • Función para llamar a la API de Pausar la lista de reproducción de Spotify.
import json  
import boto3  
import base64  
from botocore.exceptions import ClientError  
import requests  
from requests.auth import HTTPBasicAuth

def get_secret ():  
    secret_name = "<vuestro_secret_name>"
    region_name = "eu-west-1"

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    # In this sample we only handle the specific exceptions for the 'GetSecretValue' API.
    # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
    # We rethrow the exception by default.

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        if e.response['Error']['Code'] == 'DecryptionFailureException':
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InvalidParameterException':
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InvalidRequestException':
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if 'SecretString' in get_secret_value_response:
            secret = get_secret_value_response['SecretString']
            return secret
        else:
            decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
            return decoded_binary_secret

def getSpotifyOauthToken ():  
    secrets = get_secret()
    secrets_json = json.loads(secrets)

    client_id_spotify = secrets_json["spotify_client_id"]
    old_access_token = secrets_json["access_token"]
    client_secret_spotify = secrets_json["spotify_client_secret"]
    refresh_token = secrets_json["refresh_token"]

    id_secret = client_id_spotify+':'+client_secret_spotify
    client_secret_spotify_64 = base64.urlsafe_b64encode(id_secret.encode()).decode()
    headers_spotify = {
        'Authorization': 'Basic {}'.format(client_secret_spotify_64)
    }
    data_oauth = {
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token
    }

    access_response = requests.post('https://accounts.spotify.com/api/token', data=data_oauth, headers=headers_spotify)
    access_response_json = access_response.json()
    new_access_token = access_response_json["access_token"]

    return new_access_token

def pauseSpotify(access_token) :

    authorization = "Bearer "+access_token

    headers_pause = {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "Authorization": authorization
    }

    apiURL = 'https://api.spotify.com/v1/me/player/pause'

    response = requests.put(apiURL, headers=headers_pause)
    return response.text

def lambda_handler(event, context):  
    access_token = getSpotifyOauthToken()
    pauseSpotify(access_token)

    return 0

Una vez tenemos el código, creamos la función Lambda “sin vpc” para que tenga acceso a internet. También necesitaremos crear un IAM Role específico con una policy que le permita acceder únicamente al secreto que hemos creado. El json de la policy asignada al role es este en mi caso:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1571395996325",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:secretsmanager:eu-west-1:<id_de_vuestra_cuenta>:secret:<nombre_de_vuestro_secret>"
    }
  ]
}

Lo que se reflejará en el diagrama de orígenes y destinos de la Lambda en la consola:

alt

Una vez creada la función podemos desplegar la función y probar que se ejecuta correctamente. En ese punto podéis hacer pruebas de pausar la música. Así que estamos listos para configurar nuestro…

AWS IoT Button

En primer lugar accederemos desde la consola de AWS a la sección de IoT Core, y bajo Administración -> Objetos, crearemos uno nuevo:

alt

Un objeto no es más que la representación en IoT Core de un dispositivo físico. Al crearlo elegimos un dispositivo único, ya que el IoT Core también nos permite crear flotas de dispositivos. Le damos un nombre y especificamos que se trata de un IoT Button:

alt

En el siguiente paso crearemos un certificado para el dispositivo, que podemos descargar y posteriormente utilizar en la configuración del botón para que “se hablen” IoT Core y nuestro botón.

alt

Una vez creado nuestro objeto tenemos que indicarle que como acción desencadenada lance nuestra función Lambda que hemos creado antes:

alt

También tendremos que crear una policy y asociarle el certificado, para que los dispositivos tengan autorización para publicar eventos en nuestra plataforma de IoT Core.

alt

Ahora volvemos sobre nuestro objeto creado y vamos a la sección Interactuar, donde podemos ver el endpoint de IoT al que tendrá que conectarse nuestro botón:

alt

Pasamos ahora a configurar nuestro AWS IoT Button. Podemos hacerlo desde la aplicación disponible para Android e iOS o vía web desde el ordenador (como hemos hecho aquí). Como hemos comentado, funcionan conectándose a la wifi así que deberemos tener el ordenador conectado a la wifi y nuestro botón a mano.

Manteniendo el botón pulsado durante cinco segundos veremos que empieza a parpadear en azul, lo que indica que hemos entrado en modo Configuración, lo que provoca la creación de una red wifi a la que tenemos que conectar el ordenador:

alt

Una vez conectados, accedemos a la siguiente URL desde el navegador:
http://192.168.0.1/index.html

Lo que nos lleva a la pantalla de configuración donde añadiremos el endpoint de IoT, los certificados y los datos de la wifi a la que se conectará el botón:

alt

¡Y ya está, aceptamos y nuestro botón estará conectado!

alt

¡Ahora tan solo queda probarlo!

alt

Con esto he llegado al final, un poco de cinta de doble cara, y ya tenemos nuestro “Mute Button” 😊
Probablemente haya mil maneras más sencillas de pausar Spotify, pero recordad que (excepto en producción):

alt

Conclusión

Como resumen, espero que hayáis visto que esto no era un manual avanzado sobre llamadas a APIs, sobre desarrollo de Lambdas ni sobre IoT, tan solo un humilde intento de adentraros en servicios como AWS Secrets Manager y en conceptos como AWS IoT Button y cómo ejecutar Lambdas desde nuestros dispositivos IoT. A partir de ahí el campo es amplio, ¡jugad y divertíos!

Si te ha gustado, ¡síguenos en Twitter para estar al día de nuevas entregas!