Skip to content

Latest commit

 

History

History

sap_api_gateway

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

API Gateway with AWS CDK

Introduction

The architecture implemented here deploys an REST API using multiple AWS services via AWS CDK. The API logic itself is implemented in Golang and can be found here. The source code is packaged in a Docker image and uploaded to Amazon Elastic Container Repository (ECR). The architecture uses Amazon Cognito for authentication & authorization, Amazon API Gateway (API GW) for API management, Amazon Elastic Load Balancer Service for request routing, Amazon Elastic Container Service (ECS) to provide compute, and Amazon Relational Database Service (RDS) for persistent storage.
overview

Architecture

An Amazon Virtual Private Cloud (VPC) is deployed with 3 subnet groups: Public, DB, & ECS. Each subnet group has a subnet in 2 different Availability Zones (AZs), making a highly-available setup possible.
The Public subnets are "public", i.e., they have direct internet-connectivity via an internet gateway. The ECS subnets are private, but can access the internet via a NAT gateway. While the DB subnets are isolated, i.e., they are not exposed to the internet and do not have internet connectivity.

For persistent storage, a PostgreSQL Database is deployed into the VPC in the DB subnets via Amazon RDS Aurora Cluster. The cluster instances are configured to not be accessible over the internet, i.e., only traffic from within the VPC can reach the cluster.

An ECS Cluster is provisioned in the ECS subnets to execute the API logic. An ECS Fargate Service is deployed into the cluster to run a task containing a container which is created from the Docker image of the API.

To provide the ECS Service with easy reachability, an Application Load Balancer (ALB) is provisioned in the Public subnets. The default listener for the ALB is configured to route requests to the ECS Service over HTTP.

For Identity and Access Management (IAM), an Amazon Cognito User Pool is created. For easy of testing from over the CLI, an application client is added to the User Pool to facilitate authentication via the CLI.

An API GW REST API is created to provide public access to the API. The API contains a /book resource at the top level which provides a GET method (to list available books) and a POST method (to add a new book). Both of which do no use any authorizers (i.e., can be access freely).
Stemming from the /book resource is the /book/{bookId} resource. This resource offers GET, PUT, and DELETE methods which makes it possible to read, edit, and delete a book respectively. These methods are protected with a Cognito authorizer (i.e., a user must authenticate with Cognito and then pass the obtained access token via the Authorization header).
HTTP backend integrations are created for each of the provided API methods that forward requests to the ECS service via the ALB. Since the API logic requires HTTP Basic Authentication, the HTTP username/password are embedded in the HTTP integration URL.
To test the 29 second response time limit, A Lambda Integration is created at /limits/time/exceed which proxies requests to a Lambda function that sleeps for 30 seconds. Requests to this endpoint are expected to return a 504 Integration Timeout error.
Maximum permitted payload through the API Gateway service is 10 mb. To illustrate this, an AWS Service Integration that uploads a file to an S3 bucket (which is created as a part of the deployment) is created at /limits/payload/{bucket}. Uploading a file more than 10 mb via this endpoint should return a 413 HTTP content length exceeded 10485760 bytes error.
NOTE: Caching is enabled on the API Gateway with a 60 seconds TTL. architecture

Caveats

  • SSM Secure reference is not supported in: AWS::ECS::TaskDefinition/Properties/ContainerDefinitions. Hence, a Custom Resource is used to obtain the unencrypted values for the DB Password and the HTTP Basic Auth Password which are then passed to the ECS task definition.
  • There's a big issue around creating Lambda-based custom CloudFormation resources (as at CDK Version 2.93.0, github.com/aws/aws-cdk-go/awscdk/v2 v2.92.0): You should NOT use cfnresponse to send back the response to CFn, the CDK provider handles this. Only a JSON Response is required. See: aws/aws-cdk#21058.
  • The ECS Service is exposed over the internet through an ALB, this is not desirable. Rather, an NLB could be used such that a Private HTTP Integration can be created for the API Gateway.

Prerequisites

CDK Bootstrapping

Deploying stacks with AWS CDK requires some AWS resources to be available, these can be created via bootstrapping.

Requirements

CDK Context

The following CDK context values are needed:

  • dbPasswordSsmParam: (required), name of SSM Parameter that holds DB Password
  • httpBasicAuthPasswordSsmParam: (required), name of SSM Parameter that holds HTTP Basic Auth Password
  • sourceCodePath: (required), absolute path to API source code
  • dbUser: (optional), defaults to postgres
  • dbName: (optional), defaults to books
  • httpBasicAuthUser: (optional), defaults to admin

A convenience script has been created to make the process easy. To generate all required context values and add them to the context object of .cdk.json, run:

./scripts/generate_context.sh

Usage

Deployment

Run the following set of commands to deploy the solution:

cd cdk-api-gw
# Install Go modules
go mod tidy
# Generate required cdk context values
./scripts/generate_context.sh
# Synthesize CloudFormation Stacks
cdk synth
# Deploy Resources (no prompt)
cdk deploy --all --require-approval never

Sample Client Requests

After deploying the solution

# Cognito User Pool ID: Exported as CloudFormation Output "UserPoolId"
export USER_POOL_ID="your-user-pool-id"
# Cognito User Pool App Client ID: Exported as CloudFormation Output "AppClientId"
export CLIENT_APP_ID="your-app-client-id"
export EMAIL="your-email"
# Create Cognito User, sign-in and echo access token to stdout
# NOTE: Token expires in 30 minutes
./scripts/create_and_auth_cognito_user.sh $USER_POOL_ID $CLIENT_APP_ID $EMAIL
export ACCESS_TOKEN="paste-access-token-here"


# API GW Endpoint URL: Exported as CloudFormation Output "ApiGwUrl"
export API_URL="your-api-gw-url"
export BOOK_URL="${API_URL}/book"
# List books
curl -v "${BOOK_URL}" | jq

# Create book
curl -v -X POST \
  -H "Content-Type: application/json" \
  --data '{"title":"The Power of Geography","author":"Tim Marshall","year":2009}' \
  "${BOOK_URL}" | jq

# Read book
BOOK_ID="book-id"
curl -v \
  -H "Authorization: ${ACCESS_TOKEN}" \
  "${BOOK_URL}/${BOOK_ID}" | jq

# Update book
BOOK_ID="book-id"
curl -v -X PUT \
  -H "Authorization: ${ACCESS_TOKEN}"  \
  -H "Content-Type: application/json" \
  --data '{"title":"The Gods are to blame","author":"John Doe","year":1992}' \
  "${BOOK_URL}/${BOOK_ID}" | jq

# Delete book
BOOK_ID="book-id"
curl -v -X DELETE \
  -H "Authorization: ${ACCESS_TOKEN}"  \
  "${BOOK_URL}/${BOOK_ID}" | jq

## Limits
export LIMITS_URL="${API_URL}/limits"
# 29 seconds response limit
curl -v \
  "${LIMITS_URL}/time/exceed"

# 10 mb limit
# S3 Bucket Name: Exported as CloudFormation Output "S3BucketName"
export BUCKET_NAME="your-bucket-name"
export FILE_PATH="local-path-to-large-file"
curl -v \
  --data-binary @${FILE_PATH} \
  "${LIMITS_URL}/payload/${BUCKET_NAME}"

## Cache invalidation: doesn't seem to work(?)
curl -v \
  -H "Cache-Control: max-age=0" \
  "${BOOK_URL}" | jq

Errors

  • 401:
    • Unauthorized
    • Access token expired
  • 403:
    • Access denied: WAF Filtered
    • Access denied: undefined path
  • 413:
    • HTTP content length exceeded 10485760 bytes: Larger than 10 mb payload
  • 502:
    • Integration error, e.g., bad Lambda response
  • 504:
    • Integration Timeout: Backend response time > 29 seconds

CDK vs. Terraform (My Review)

Note: This is coming from someone that has been working with terraform for some years

CDK Pros

  • Packaging code is easy when compared to the experiences I've had with terraform
  • Automated creation of IAM permissions & roles with L2 Constructs, and makes it easy to grant permissions using the .Grant() feature

CDK Cons

  • Resource definition with CDK is more verbose than with terraform
  • Non-breaking changes are harder to avoid in CDK compared to terraform
  • Some broken deployments often require manual cleanup. I.e., manually deleting the CloudFormation stack. I've rarely experienced this with terraform
  • Passing around the "scope" seems redundant compared to terraform's implicit knowledge of the current "scope"