📡 AWS CDK 101 ⛄️ - API Gateway construct usage, throttle, quota, usage plans, api keys

📡 AWS CDK 101 ⛄️ - API Gateway construct usage, throttle, quota, usage plans, api keys

·

9 min read

🔰 Beginners new to AWS CDK, please do look at my previous articles one by one in this series.

If incase missed the previous article do find it with the below links.

🔁 Original previous post at 🔗 Dev Post

🔁 Reposted previous post at 🔗 dev to @aravindvcyber

In this article let us introduce an API Gateway in front of our basic lambda function which we have created in our last article above listed above.

Why API Gateway in our solution ❓

Since our lambda is only accessible inside our aws environment, we require this API Gateway configuration which will help expose a public HTTP endpoint that anyone on the internet can hit with an HTTP client.

Besides this an API Gateway can block improper requests without invoking the backend Lambda functions, when authorization checks fail. API Gateway can save expensive Lambda invocation costs in this way and can also offload request validation from your Lambda function as well, based on the requirement.

Let us start by editing the lib/common-event-stact.ts file. Here let us import the modules from aws-cdk-lib/aws-apigateway as agigw.

 // this defines an new API Gateway REST API resource backed by our "eventEntry" function.
    const eventGateway = new apigw.LambdaRestApi(this, 'EventEndpoint', {
      handler: eventEntry
    });

When you run cdk diff, you can expect similar log on the new resources to be created as well as the IAM policy changes involved.

IAM Statement Changes
┌───┬─────────────────────────────────────────────────────┬────────┬───────────────────────┬─────────────────────────────────────────────────────┬─────────────────────────────────────────────────────┐
│   │ Resource                                            │ Effect │ Action                │ Principal                                           │ Condition                                           │
├───┼─────────────────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────────────────┼─────────────────────────────────────────────────────┤
│ + │ ${EventEndpoint/CloudWatchRole.Arn}                 │ Allow  │ sts:AssumeRole        │ Service:apigateway.amazonaws.com                    │                                                     │
├───┼─────────────────────────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────────────────────────┼─────────────────────────────────────────────────────┤
│ + │ ${EventEntryHandler.Arn}                            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                    │ "ArnLike": {                                        │
│   │                                                     │        │                       │                                                     │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-a │
│   │                                                     │        │                       │                                                     │ pi:${AWS::Region}:${AWS::AccountId}:${EventEndpoint │
│   │                                                     │        │                       │                                                     │ 82CBF40A}/${EventEndpoint/DeploymentStage.prod}/*/* │
│   │                                                     │        │                       │                                                     │ "                                                   │
│   │                                                     │        │                       │                                                     │ }                                                   │
│ + │ ${EventEntryHandler.Arn}                            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                    │ "ArnLike": {                                        │
│   │                                                     │        │                       │                                                     │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-a │
│   │                                                     │        │                       │                                                     │ pi:${AWS::Region}:${AWS::AccountId}:${EventEndpoint │
│   │                                                     │        │                       │                                                     │ 82CBF40A}/test-invoke-stage/*/*"                    │
│   │                                                     │        │                       │                                                     │ }                                                   │
│ + │ ${EventEntryHandler.Arn}                            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                    │ "ArnLike": {                                        │
│   │                                                     │        │                       │                                                     │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-a │
│   │                                                     │        │                       │                                                     │ pi:${AWS::Region}:${AWS::AccountId}:${EventEndpoint │
│   │                                                     │        │                       │                                                     │ 82CBF40A}/${EventEndpoint/DeploymentStage.prod}/*/" │
│   │                                                     │        │                       │                                                     │ }                                                   │
│ + │ ${EventEntryHandler.Arn}                            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                    │ "ArnLike": {                                        │
│   │                                                     │        │                       │                                                     │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-a │
│   │                                                     │        │                       │                                                     │ pi:${AWS::Region}:${AWS::AccountId}:${EventEndpoint │
│   │                                                     │        │                       │                                                     │ 82CBF40A}/test-invoke-stage/*/"                     │
│   │                                                     │        │                       │                                                     │ }                                                   │
└───┴─────────────────────────────────────────────────────┴────────┴───────────────────────┴─────────────────────────────────────────────────────┴─────────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬─────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                        │ Managed Policy ARN                                                                      │
├───┼─────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${EventEndpoint/CloudWatchRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs │
└───┴─────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[+] AWS::ApiGateway::RestApi EventEndpoint EventEndpoint82CBF40A 
[+] AWS::IAM::Role EventEndpoint/CloudWatchRole EventEndpointCloudWatchRole4E252113 
[+] AWS::ApiGateway::Account EventEndpoint/Account EventEndpointAccount1A8CF478 
[+] AWS::ApiGateway::Deployment EventEndpoint/Deployment EventEndpointDeployment9E6BA6B96f1395a2ccbaf49542e68813f77a19e7 
[+] AWS::ApiGateway::Stage EventEndpoint/DeploymentStage.prod EventEndpointDeploymentStageprod2ED092D9 
[+] AWS::ApiGateway::Resource EventEndpoint/Default/{proxy+} EventEndpointproxyCBF9AFD2 
[+] AWS::Lambda::Permission EventEndpoint/Default/{proxy+}/ANY/ApiPermission.CommonEventStackEventEndpointB06FCC50.ANY..{proxy+} EventEndpointproxyANYApiPermissionCommonEventStackEventEndpointB06FCC50ANYproxy3F9D95E2 
[+] AWS::Lambda::Permission EventEndpoint/Default/{proxy+}/ANY/ApiPermission.Test.CommonEventStackEventEndpointB06FCC50.ANY..{proxy+} EventEndpointproxyANYApiPermissionTestCommonEventStackEventEndpointB06FCC50ANYproxy75C60637 
[+] AWS::ApiGateway::Method EventEndpoint/Default/{proxy+}/ANY EventEndpointproxyANY74421EDC 
[+] AWS::Lambda::Permission EventEndpoint/Default/ANY/ApiPermission.CommonEventStackEventEndpointB06FCC50.ANY.. EventEndpointANYApiPermissionCommonEventStackEventEndpointB06FCC50ANY7F8C8232 
[+] AWS::Lambda::Permission EventEndpoint/Default/ANY/ApiPermission.Test.CommonEventStackEventEndpointB06FCC50.ANY.. EventEndpointANYApiPermissionTestCommonEventStackEventEndpointB06FCC50ANYC6DC815A 
[+] AWS::ApiGateway::Method EventEndpoint/Default/ANY EventEndpointANY8620E9D3

Summarizing the above output 💥

To summarize the above, let us understand what happens in the backend as follows and will be published.

  • A new IAM policy is created CloudWatchRole and assumed to push logs to cloudwatch from apigateway.
  • A deployment stage prod is setup, we can have multiple stage for premature feature testing with this, without affecting the prod stage.
  • Resource permissions are configured with necessary privileges for the any Resource path, by default a greedy resource path {proxy+} for both normal endpoint and test endpoint for this rest api.
  • Resource permissions are configured with necessary privileges for the any method for both normal endpoint and test variant endpoint for this rest api.

The deep understanding on the above concepts will help us when we further restrict the Api gateway in future for similar rest based endpoints.

Now let us execute this deployment and checkout the results.

Before this let me do one more change in our lambda to display appropriate message.

exports.receiver = async function(event:any) {
  console.log("request:", JSON.stringify(event, undefined, 2));
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    body: `Message Received in lambda: ${JSON.stringify(event.body)}\n`
  };
};

cdk deploy CommonEventStack



 ✅  CommonEventStack

✨  Deployment time: 57.21s

Outputs:
CommonEventStack.EventEndpointA893C53E = https://lwo***********uvhg.execute-api.ap-south-1.amazonaws.com/prod/
Stack ARN:
arn:aws:cloudformation:ap-south-1:575066707855:stack/CommonEventStack/93688c10-a10a-11ec-b942-0ad3136ae540

✨  Total time: 68.43s

🏃 Curl tests

When we do a curl to check this rest endpoint let us see what we get.

curl https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/

Message Received: null

Here we are successful in sending message to the lambda and also we got some output. Though we have null because we never sent any body in our initial GET request.

Now let us send some message via the body of the request from client in a post request.

curl -H "Content-Type: application/json" -d '{ "message": "client message"}' -X POST https://********.execute-api.ap-south-1.amazonaws.com/prod/

Message Received in lambda: "{ \"message\": \"client message\"}"

We have successfully setup the agi gateway, so let us check it in the console and test it in the aws console as well.

API gateway in AWS console ⚡️

API gateway in aws console

Inspect the endpoint path and the methods allowed⚡️

endpoint path and the methods

Test Invocation POST Request with message ⚡️

Test Invocation POST Request with message

Refining the Api gateway created above ⚡️

Now let us refine api gateway created to be available only for the POST method and a specific resource path only.

CDK API Reference documentation 🐼

Before we do that, we have to make use of the cdk api reference documentation. Hence forth we can explore this reference documentation to pick the necessary params and options for every cdk stack resource construction.

Here if you remember we have imported aws-cdk-lib/aws-apigateway, we need to learn to use the construct library at https://docs.aws.amazon.com/cdk/api/v2

Specifically we have to navigate to https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway-readme.html. Here we could find the necessary documentation for the apigateway construct and how to use its properties and methods, with suitable examples.

Specify the right resource path and method 🔦

First let us remove this greedy proxy resource path definition.

const eventGateway = new apigw.LambdaRestApi(this, 'EventEndpoint', {
      handler: eventEntry,
      proxy: false
    });

Here the proxy attribute removes the greedy resource path definition by default.

Now let us declare an available resource path like the below example.

 const eventHandler: apigw.LambdaIntegration = new apigw.LambdaIntegration(eventEntry);
    const event = eventGateway.root.addResource('event');

    const eventMethod: apigw.Method = event.addMethod('POST', eventHandler, {  
       apiKeyRequired: false
    });

Event Resource POST request test

Now we are only exposing and allowing specific resource path and method successfully.

IAM policy changes

When we access the base resource path with forbidden methods 🔦

Missing authentication token

Whereas we can access our specific api successfully, note the difference in the full path with the allowed method.

Successful resource invocation call

Usage Plan & API Keys 🔔

A usage plan specifies who can access one or more deployed API stages and methods, and the rate at which they can be accessed.

The plan uses API keys to identify API clients and meters access to the associated API stages for each key accordingly. Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys.

The following example shows how to create and associate a usage plan and an API key:

const eventMethod: apigw.Method = event.addMethod('POST', eventHandler, {  
       apiKeyRequired: true,
    });

No key provided forbidden

In order to use a api key it is required to setup an usage plan, something similar to the below example and then we can associate that to the key we just created.


const plan = eventGateway.addUsagePlan('UsagePlan', {
        name: 'Workshop',
        description: 'For Workshop',
        throttle: {
          burstLimit: 5,
          rateLimit: 10,
        },
        quota: {
          limit: 100,
          period: apigw.Period.DAY
        }
    });

    const normalUserKey = eventGateway.addApiKey('ApiKey',{
        apiKeyName: 'av-api-key',
        value: 'av-api-key-value-********',
    });
    plan.addApiKey(normalUserKey);

key provided and successful

Deployment stage level fine-tuning the usage plan 🌻

Fine-tuning the above usage plan at various deployment stages for the specific method additionally and including method level throttle limits helps with greater control on the api consumption limits and greater resource utilisation with multiple clients based on contract.

 plan.addApiStage({
      stage: eventGateway.deploymentStage,
      throttle: [
        {
          method: eventMethod,
          throttle: {
            rateLimit: 3,
            burstLimit: 2
          }
        }
      ]
    });

Throttling the api with rate and burst limit 🐇

You can effectively control the rate of the number of invocations per seconds by rate limit before the gateway deny any further requests at the entry itself.

  • The burst limit defines the number of requests your API can handle concurrently.
  • The rate limit defines the number of allowed requests per second.
 throttle: {
            rateLimit: 3,
            burstLimit: 2
          }

Rate limiting key

Quota limit 🐡

The quota limit helps to define the cumulative number of client request received by the api gateway before the gateway limit exceeded.

Generally aws provides certain fixed quota for every service per region, this can be effectively managed by reserving a part to every usage plan as desired. This can also check to limit the external user api consumption, based on their service tier.

 quota: {
      limit: 5,
      period: apigw.Period.DAY
      }

Once the quota allocated for the api key user is exceeded, the error responses are sent.

Quota Exceeded

Api Key Usage

Rate Limited API Key 🐠

In certain scenarios where you need to create a single api key and configure rate limiting for it, you can use RateLimitedApiKey. This construct lets you specify rate limiting properties which should be applied only to the api key being created. The API key created has the specified rate limits, such as quota and throttles, applied, even without any association to a usage plan.

const rateLimitedKey = new apigw.RateLimitedApiKey(this, 'rate-limited-api-key', {
       apiKeyName: 'av-rate-limited-api-key',
        value: 'av-api-key-value-dreatenbwebrwiukjwnrn',
    customerId: 'external-client',
    resources: [eventGateway],
    quota: {
      limit: 5,
      period: apigw.Period.DAY
      },
      throttle: {
        rateLimit: 2,
        burstLimit: 1
      }
    });
    plan.addApiKey(rateLimitedKey);

We could now check the same in the api console, where we can find a new plan is created for the newly created api key. For further fine control for single users not part of any other usage plans.

Rate limiting key settings

Rate limiting key

Importing the existing keys and usage plans 🐝

Besides this we can also import the existing keys and usage plan into the our stack resources by following the below examples.

usage plans can be imported into a CDK app using its id.

const importedUsagePlan = apigateway.UsagePlan.fromUsagePlanId(this, 'imported-usage-plan', '<usage-plan-key-id>');

API keys can also be imported into a CDK app using its id.

const importedKey = apigateway.ApiKey.fromApiKeyId(this, 'imported-key', '<api-key-id>');

We will add more connections to this api gateway and lambda and make it more usable in the upcoming articles stay subscribed.

⏭ We have our next article in serverless, do check out

🎉 Thanks for supporting! 🙏

Would be really great if you like to ☕️ Buy Me a Coffee, to help boost my efforts.

🔁 Original post at 🔗 Dev Post

🔁 Reposted at 🔗 dev to @aravindvcyber

Did you find this article valuable?

Support Aravind V by becoming a sponsor. Any amount is appreciated!