L’API Gateway est un service AWS qui permet de déployer une API disponible sur un endpoint. Nous allons voir dans cet article comment déclencher une fonction Lambda au travers de l’API Gateway. En d’autres termes, nous allons détailler comment implémenter une API avec des fonctions Lambda.

La fonction Lambda

Lors du précédent article, nous avons vu comment créer une fonction Lambda sur AWS. Nous allons faire évoluer cette fonction pour quelle retourne un document JSON. Voici le nouveau code de hello_you.js :

console.log('Loading function');

exports.handler = function(event, context, callback) {
  console.log('Received event:', JSON.stringify(event, null, 2));
  callback(null, '{"message": "Hello ' + event.name + '"}');
};

Pour rappel, cette fonction se crée de cette façon :

$ aws iam create-role \
  --role-name myproject-lambdas \
  --assume-role-policy-document file://./lambdas-role.json

{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": {
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {
                    "Service": "lambda.amazonaws.com"
                }
            }
        },
        "RoleId": "AROAIVDVXX57QTPYN7CFM",
        "CreateDate": "2016-06-22T06:48:38.870Z",
        "RoleName": "myproject-lambdas",
        "Path": "/",
        "Arn": "arn:aws:iam::client-id:role/myproject-lambdas"
    }
}

$ aws iam put-role-policy \
  --role-name myproject-lambdas \
  --policy-name myproject-lambdas-policy \
  --policy-document file://./lambdas-policy.json

$ CLIENT_ID=xxxxxxxxxxxx
$ aws lambda create-function \
  --function-name MyProjectHelloYouOverHttps \
  --zip-file fileb://./hello_you.zip \
  --role arn:aws:iam::$CLIENT_ID:role/myproject-lambdas \
  --handler hello_you.handler \
  --runtime nodejs4.3

{
    "FunctionName": "MyProjectHelloYouOverHttps",
    "CodeSize": 309,
    "MemorySize": 128,
    "FunctionArn": "arn:aws:lambda:eu-west-1:client-id:function:MyProjectHelloYouOverHttps",
    "Handler": "hello_you.handler",
    "Role": "arn:aws:iam::client-id:role/myproject-lambdas",
    "Timeout": 3,
    "LastModified": "2016-06-22T06:49:15.737+0000",
    "Runtime": "nodejs4.3",
    "Description": ""
}

Maintenant que la fonction Lambda est déployée et fonctionnelle, il faut créer une API qui déclenchera son exécution.

API Gateway

La fonction Lambda sera déclenchée par un appel POST sur la ressource /MyProjectHelloYou. Notez que le sujet ici n’est pas de respecter les bonnes pratiques de design d’API RESTful. 😉

La création de cette API se fait en plusieurs étapes :

# création de l’API
$ aws apigateway create-rest-api \
  --name MyProjectHelloYouApi

{
    "name": "MyProjectHelloYouApi",
    "id": "xxxxxxxxxx",
    "createdDate": 1466571467
}

# récupération de l’identifiant de la racine
$ API_ID=xxxxxxxxxx
$ aws apigateway get-resources \
  --rest-api-id $API_ID

{
    "items": [
        {
            "path": "/",
            "id": "xxxxxxxxxx"
        }
    ]
}

# création de la ressource
$ ROOT_ID=xxxxxxxxxx
$ aws apigateway create-resource \
  --rest-api-id $API_ID \
  --parent-id $ROOT_ID \
  --path-part MyProjectHelloYou

{
    "path": "/MyProjectHelloYou",
    "pathPart": "MyProjectHelloYou",
    "id": "xxxxxx",
    "parentId": "xxxxxxxxxx"
}

# création de la méthode POST 
$ RESOURCE_ID=xxxxxxxxxx
$ aws apigateway put-method \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --api-key-required \
  --authorization-type NONE

{
    "apiKeyRequired": true,
    "httpMethod": "POST",
    "authorizationType": "NONE"
}

Ensuite, il faut faire pointer l’API sur la fonction Lambda et mettre un peu de glue pour définir les formats de sortie de ces 2 services :

# pointe l’API vers la lambda
$ aws apigateway put-integration \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --type AWS \
  --integration-http-method POST \
  --uri arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-1:$CLIENT_ID:function:MyProjectHelloYouOverHttps/invocations

{
    "httpMethod": "POST",
    "passthroughBehavior": "WHEN_NO_MATCH",
    "cacheKeyParameters": [],
    "type": "AWS",
    "uri": "arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-1:client-id:function:MyProjectHelloYouOverHttps/invocations",
    "cacheNamespace": "xxxxxx"
}

# configure le type de réponse de l’API
$ aws apigateway put-method-response \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --status-code 200 \
  --response-models "{\"application/json\": \"Empty\"}"

{
    "responseModels": {
        "application/json": "Empty"
    },
    "statusCode": "200"
}

# configure le type de réponse de la fonction Lambda
$ aws apigateway put-integration-response \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --status-code 200 \
  --response-templates "{\"application/json\": \"\"}"

{
    "statusCode": "200",
    "responseTemplates": {
        "application/json": null
    }
}

Maintenant, il est nécessaire de déployer cette API afin de pouvoir la tester sur un environnement de test (appelé “dev” dans cet exemple). Cette étape se compose encore d’une série d’instructions :

# déploiement de l’API
$ aws apigateway create-deployment \
  --rest-api-id $API_ID \
  --stage-name dev

{
    "id": "xxxxxx",
    "createdDate": 1466571509
}

# autorise l’API à appeler la fonction lambda pour les tests
$ aws lambda add-permission \
  --function-name MyProjectHelloYouOverHttps \
  --statement-id apigateway-test-2 \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:eu-west-1:$CLIENT_ID:$API_ID/*/POST/MyProjectHelloYou"

{
    "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:eu-west-1:client-id:xxxxxx/*/POST/MyProjectHelloYou\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:eu-west-1:client-id:function:MyProjectHelloYouOverHttps\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"apigateway-test-2\"}"
}

# autorise l’API déployée à appeler la fonction lambda
$ aws lambda add-permission \
  --function-name MyProjectHelloYouOverHttps \
  --statement-id apigateway-dev-2 \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:eu-west-1:$CLIENT_ID:$API_ID/dev/POST/MyProjectHelloYou"

{
    "Statement": "{\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:execute-api:eu-west-1:client-id:xxxxxxxxxx/dev/POST/MyProjectHelloYou\"}},\"Action\":[\"lambda:InvokeFunction\"],\"Resource\":\"arn:aws:lambda:eu-west-1:client-id:function:MyProjectHelloYouOverHttps\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"apigateway.amazonaws.com\"},\"Sid\":\"apigateway-dev-2\"}"
}

Avant de pouvoir appeler l’API, il faut encore créer une clé d’API. En effet, lors de la création de l’API, il avait été précisé que l’API n’était accessible qu’avec une clé d’API. Cette dernière se génère assez facilement :

$ aws apigateway create-api-key \
  --name MyProjectHelloYouKey \
  --enabled \
  --stage-keys restApiId=$API_ID,stageName=dev

{
    "name": "MyProjectHelloYouKey",
    "createdDate": 1466571553,
    "lastUpdatedDate": 1466571553,
    "enabled": true,
    "id": "WxNEtx7HyG7NGYUDXszS87HradBHwSl78mTSicgi",
    "stageKeys": [
        "xxxxxxxxxx/dev"
    ]
}

Invocation de l’API

Comme toujours sur AWS, ce service est invocable via le client en ligne de commande :

# appelle l’API avec le client AWS
$ aws apigateway test-invoke-method \
  --rest-api-id $API_ID \
  --resource-id $RESOURCE_ID \
  --http-method POST \
  --path-with-query-string "" \
  --body "{\"name\":\"you\"}"

{
    "status": 200,
    "body": "\"{\\\"message\\\": \\\"Hello you\\\"}\"",
    "log": "Execution log for request test-request\nTue Jun 22 05:08:00 UTC 2016 : Starting execution for request: test-invoke-request\nTue Jun 22 05:08:00 UTC 2016 : HTTP Method: POST, Resource Path: /MyProjectHelloYou\nTue Jun 22 05:08:00 UTC 2016 : Method request path: {}\nTue Jun 22 05:08:00 UTC 2016 : Method request query string: {}\nTue Jun 22 05:08:00 UTC 2016 : Method request headers: {}\nTue Jun 22 05:08:00 UTC 2016 : Method request body before transformations: {\"name\":\"you\"}\nTue Jun 22 05:08:00 UTC 2016 : Endpoint request URI: https://lambda.eu-west-1.amazonaws.com/2015-03-31/functions/arn:aws:lambda:eu-west-1:client-id:function:MyProjectHelloYouOverHttps/invocations\nTue Jun 22 05:08:00 UTC 2016 : Endpoint request headers: {x-amzn-lambda-integration-tag=test-request, Authorization=************************************************************************************************************************************************************************************************************************************************************************************************************************cd3e39, X-Amz-Date=20160622T050800Z, x-amzn-apigateway-api-id=xxxxxxxxxx, X-Amz-Source-Arn=arn:aws:execute-api:eu-west-1:client-id:xxxxxxxxxx/null/POST/MyProjectHelloYou, Accept=application/json, User-Agent=AmazonAPIGateway_xxxxxxxxxx, Host=lambda.eu-west-1.amazonaws.com, X-Amz-Content-Sha256=9835a899d4c199ed9a5f9783bd807e3add451bca4d7d9bc57747b912d629bbde, Content-Type=application/json}\nTue Jun 22 05:08:00 UTC 2016 : Endpoint request body after transformations: {\"name\":\"you\"}\nTue Jun 21 12:08:00 UTC 2016 : Endpoint response body before transformations: \"{\\\"message\\\": \\\"Hello you\\\"}\"\nTue Jun 21 12:08:00 UTC 2016 : Endpoint response headers: {x-amzn-Remapped-Content-Length=0, x-amzn-RequestId=cc2ccc9a-37a8-11e6-8766-67f2e954c41a, Connection=keep-alive, Content-Length=30, Date=Tue, 22 Jun 2016 05:08:00 GMT, Content-Type=application/json}\nTue Jun 22 05:08:00 UTC 2016 : Method response body after transformations: \"{\\\"message\\\": \\\"Hello you\\\"}\"\nTue Jun 22 05:08:00 UTC 2016 : Method response headers: {Content-Type=application/json}\nTue Jun 22 05:08:00 UTC 2016 : Successfully completed execution\nTue Jun 22 05:08:00 UTC 2016 : Method completed with status: 200\n",
    "latency": 310,
    "headers": {
        "Content-Type": "application/json"
    }
}

Bien entendu, il est possible d’appeler cette API avec un client HTTP tel que curl :

# appelle l’API avec un client HTTP sans API Key pour vérifier la sécurisation
curl -i -X POST -d "{\"name\":\"you\"}" https://$API_ID.execute-api.eu-west-1.amazonaws.com/dev/MyProjectHelloYou

HTTP/1.1 403 Forbidden
Content-Type: application/json
Content-Length: 24
Connection: keep-alive
Date: Tue, 22 Jun 2016 05:27:05 GMT
x-amzn-RequestId: 772d6e8a-37ab-11e6-a9a8-3fc9fffbc649
X-Cache: Error from cloudfront
Via: 1.1 4acfc8f4bacd56d7d02d506d28c5f05b.cloudfront.net (CloudFront)
X-Amz-Cf-Id: ycIgaavjT05o_NXNXTfvgIWeryIqyKDWSGajWAnR-SVjA3OVP-E67Q==

{"message": "Forbidden"}

# appelle l’API avec un client HTTP avec API Key
curl -i -X POST -d "{\"name\":\"you\"}" -H "x-api-key: WxNEtx7HyG7NGYUDXszS87HradBHwSl78mTSicgi" https://$API_ID.execute-api.eu-west-1.amazonaws.com/dev/MyProjectHelloYou

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 30
Connection: keep-alive
Date: Tue, 22 Jun 2016 05:29:39 GMT
x-amzn-RequestId: d29d05f7-37ab-11e6-b448-af8233f7cdf0
X-Cache: Miss from cloudfront
Via: 1.1 03e6befea808055ee570b009f21a5046.cloudfront.net (CloudFront)
X-Amz-Cf-Id: g7FsteY3jAcj1dLzatep9DcUUsKmdHx5LMmSd_xdTpMkgBrjLd4jWg==

"{\"message\": \"Hello you\"}"

Nettoyage

Après avoir testé l’intégration de l’API Gateway et AWS Lambda, un petit nettoyage s’impose.

$ aws apigateway delete-api-key \
  --api-key WxNEtx7HyG7NGYUDXszS87HradBHwSl78mTSicgi

$ aws apigateway delete-rest-api \
  --rest-api-id $API_ID

$ aws logs delete-log-group \
  --log-group-name /aws/lambda/MyProjectHelloYouOverHttps

$ aws lambda delete-function \
  --function-name MyProjectHelloYouOverHttps

$ aws iam delete-role-policy \
  --role-name myproject-lambdas \
  --policy-name myproject-lambdas-policy     

$ aws iam delete-role \
  --role-name myproject-lambdas

Conclusion

Avec cet exemple, nous venons de voir la simplicité pour mettre en ligne une API avec le service API Gateway. Ce dernier s’interface très bien avec AWS Lambda même si cela se traduit par un certain nombre d’opérations. Vous êtes maintenant capable d’implémenter et déployer une API REST sécurisée sans installer ni configurer aucun serveur !