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.
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.
- 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.
Deploying stacks with AWS CDK requires some AWS resources to be available, these can be created via bootstrapping.
- CLI environment authenticated with valid AWS credentials
- AWS CDK Toolkit
The following CDK context values are needed:
dbPasswordSsmParam
: (required), name of SSM Parameter that holds DB PasswordhttpBasicAuthPasswordSsmParam
: (required), name of SSM Parameter that holds HTTP Basic Auth PasswordsourceCodePath
: (required), absolute path to API source codedbUser
: (optional), defaults topostgres
dbName
: (optional), defaults tobooks
httpBasicAuthUser
: (optional), defaults toadmin
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
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
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
- 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
Note: This is coming from someone that has been working with terraform for some years
- 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
- 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"