All posts by Biff Gaut

Instant Well-Architected CDK Resources with Solutions Constructs Factories

Post Syndicated from Biff Gaut original https://aws.amazon.com/blogs/devops/instant-well-architected-cdk-resources-with-solutions-constructs-factories/

For several years, AWS Solutions Constructs have helped thousands of AWS Cloud Development Kit (CDK) users accelerate their creation of well-architected workloads by providing small, composable patterns linking two or more AWS services, such as an Amazon S3 bucket triggering an AWS Lambda function. Over this time, customers with use cases that don’t match an existing Solutions Construct have expressed a desire to create individual well-architected resources directly. Solutions Constructs Factories allow clients to create well-architected individual resources using the same internal code that Solutions Constructs use when composing larger patterns. While deploying a single AWS resource using the AWS CDK is often a trivial task, deploying that resource following all the best practices requires more knowledge and effort. For instance, a properly configured S3 bucket should include versioning, encryption, access logging, bucket policies allowing only TLS calls, and lifecycle policies. The AWS Solutions Constructs S3BucketFactory()method implements a fully well-architected CDK S3 bucket with all the best practices configured, including an additional bucket to hold S3 Access Logs. At the same time, every aspect of each resource can be overridden to satisfy any unique requirements for your use case. Best of all, the resources created are all standard CDK L2 constructs that integrate with any CDK stack.

Solutions Constructs Factories are a new feature of AWS Solutions Constructs, a library of common architectural patterns built on top of the AWS CDK. These multi-service patterns allow you to deploy multiple resources with a single CDK Construct. Solutions Constructs follow best practices by default – both for the configuration of the individual resources as well as their interaction. While each Solutions Construct implements a very small architectural pattern, they are designed so that multiple constructs can be combined by sharing a common resource. For instance, a Solutions Construct that implements an S3 bucket invoking a Lambda function can be deployed along with a second Solutions Construct that deploys a Lambda function that writes to an Amazon SQS queue by sharing the same Lambda function between the two constructs. There are currently over 70 Solutions Constructs available, covering Amazon API Gateway, Amazon Simple Storage Service (S3), Amazon SNS, Amazon Simple Queue Service (SQS), AWS Lambda, AWS Secrets Manager, IoT, Amazon ElasticCache, AWS Step Functions, AWS Fargate, AWS WAF, Application Load Balancers, Amazon Kinesis, Amazon Data Firehose, Amazon Sagemaker, AWS Elemental Mediastore, and more.

The AWS CDK provides a programming model above the static AWS CloudFormation template, representing all AWS resources with instantiated objects in a high-level programming language. When you instantiate CDK objects in your Typescript (or other language) code, the CDK “compiles” those objects into a JSON template, then deploys that template with CloudFormation. The use of programming languages such as Typescript or Python rather than the declarative YAML or JSON allows much more flexibility in defining your infrastructure. If you are not yet familiar with the AWS CDK you should check it out.

Visual representation of how AWS Solutions Constructs build abstractions upon the AWS CDK, which is then compiled into static CloudFormation templates. Solutions Constructs Factories are a feature found within AWS Solutions Constructs

Infrastructure as Code Abstraction Layers

How Constructs Factories Work

This demo will guide you through building a small CDK stack that uses one of the new Solutions Constructs Factories. The app will use a factory to create an S3 bucket that fully implements all the recommended best practices with a single function call.

First, create a Typescript CDK app and install the Solutions Constructs libraries:

md factories-blog
cd factories-blog
cdk init -l=typescript
npm install @aws-solutions-constructs/aws-constructs-factories

Next, open lib/factories-blog-stack.ts, delete the comments and add the indicated new lines of code:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// Add this import statement:
import { ConstructsFactories } from '@aws-solutions-constructs/aws-constructs-factories';

export class FactoriesBlogStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Add these two lines
    const factories = new ConstructsFactories(this, 'constructs-factories');
    const response = factories.s3BucketFactory('default-bucket', {});
  }
}

Now build and deploy the app:

npm run build
cdk deploy

A key consideration for Constructs Factories is that the resources they create integrate seamlessly with the rest of your CDK stack. That’s why factories are implemented as methods that return standard AWS CDK L2 constructs rather than L3 constructs. Instantiating the ConstructsFactories object gives you access to the factory methods, but on its own adds nothing to your stack. In this case you called s3BucketFactory(), passing two arguments: a string id upon which all resource names will be based, and an empty S3BucketFactoryProps object. The method added 4 resources to your CDK stack. You can explore everything this app just deployed either in the console or through the synthesized template in ./cdk.out. Here’s a synopsis of what you’ll find:

  • An S3 bucket for storage (the goal of the call), with the following configuration:
    • AES-256 server side encryption
    • All public ACLs and policies blocked
    • Versioning enabled
    • A bucket policy blocking all access not via aws:SecureTransport (TSL)
    • Access logging enabled to a separate bucket
    • A lifecycle rule transitioning non-current versions to Glacier after 90 days
  • A second S3 bucket to contain the access logs, with a similar configuration:
    • AES-256 server side encryption
    • All public ACLs and policies blocked
    • Versioning enabled
    • A bucket policy blocking all access not via aws:SecureTransport (TSL)

A single call has deployed two resources with all best practices configured by default! As most workloads have some unique requirements, all of these resources can be customized to integrate into the overall stack. Diving a little deeper, recall the empty S3BucketFactoryProps object you passed to the factory call. The S3BucketFactoryProps argument allows you to send any properties to override the default settings for the main bucket and the logging bucket. For instance, if you need the bucket to send notifications to EventBridge you can enable that through the properties:

const response = factories.s3BucketFactory('customized-bucket', {
  bucketProps: {
    eventBridgeEnabled:true
  }
});

Second, the factory call returned an S3FactoryResponse object, which exposes standard CDK L2 Bucket constructs for both the main and logging buckets (you can also access the new BucketPolicies through these Bucket constructs).

const arn = response.s3Bucket.bucketArn;

Available Factories

The first release of Solutions Constructs Factories focuses on services where configuring a resource according to best practices requires the launch of additional resources, or other additional complexity. We currently support 3 different resources:

S3 Buckets

The s3BucketFactory() function will create an S3 bucket with:

  • TLS access required
  • Access logging enabled (to an additional log bucket by default)
  • Versioning enabled
  • All public ACLs and policies blocked
  • AWS managed server side encryption
  • Lifecycle policies that transition non-current versions to S3 Glacier after 90 days

Step Functions State Machines

The stateMachineFactory() function will create a Step Functions state machine with:

  • CloudWatch logs names prefixed with /aws/vendedlogs/ and unique to your stack, avoiding any issues with resource policy size without risk of name collisions in your account.
  • CloudWatch alarms for:
    • 1 or more failed executions
    • 1 or more throttled executions
    • 1 or more aborted executions

SQS Queues

The sqsQueueFactory() function will create an SQS queue with:

  • KMS managed encryption
  • A DLQ configured to accept messages that can’t be processed
  • A resource policy ensuring that only the queue owner can perform operations on the queue

Conclusion

A single call to an AWS Solutions Constructs Factory deploys a resource with all best practice settings, as well as any additional infrastructure required to make the resource part of a well-architected application. Since the factories return standard AWS CDK L2 constructs, you can use them in any new or existing CDK stack. The first Solutions Constructs Factories were launched earlier this year and more will be added soon. If you have factories you’d like to see implemented, enter an Issue on the Solutions Constructs github page. The next time you launch any of these three resources in a stack, try using a Constructs factory – you may never directly instantiate a CDK construct again.

Build APIs using OpenAPI, the AWS CDK and AWS Solutions Constructs

Post Syndicated from Biff Gaut original https://aws.amazon.com/blogs/devops/build-apis-using-openapi-the-aws-cdk-and-aws-solutions-constructs/

Introduction

APIs are the key to implementing microservices that are the building blocks of modern distributed applications. Launching a new API involves defining the behavior, implementing the business logic, and configuring the infrastructure to enforce the behavior and expose the business logic. Using OpenAPI, the AWS Cloud Development Kit (AWS CDK), and AWS Solutions Constructs to build your API lets you focus on each of these tasks in isolation, using a technology specific to each for efficiency and clarity.

The OpenAPI specification is a declarative language that allows you to fully define a REST API in a document completely decoupled from the implementation. The specification defines all resources, methods, query strings, request and response bodies, authorization methods and any data structures passed in and out of the API. Since it is decoupled from the implementation and coded in an easy-to-understand format, this specification can be socialized with stakeholders and developers to generate buy-in before development has started. Even better, since this specification is in a machine-readable syntax (JSON or YAML), it can be used to generate documentation, client code libraries, or mock APIs that mimic an actual API implementation. An OpenAPI specification can be used to fully configure an Amazon API Gateway REST API with custom AWS Lambda integration. Defining the API in this way automates the complex task of configuring the API, and it offloads all enforcement of the API details to API Gateway and out of your business logic.

The AWS CDK provides a programming model above the static AWS CloudFormation template, representing all AWS resources with instantiated objects in a high-level programming language. When you instantiate CDK objects in your Typescript (or other language) code, the CDK “compiles” those objects into a JSON template, then deploys that template with CloudFormation. I’m not going to spend a lot of time extolling the many virtues of the AWS CDK here, suffice it to say that the use of programming languages such as Typescript or Python rather than the declarative YAML or JSON allows much more flexibility in defining your infrastructure.

AWS Solutions Constructs is a library of common architectural patterns built on top of the AWS CDK. These multi-service patterns allow you to deploy multiple resources with a single CDK Construct. Solutions Constructs follow best practices by default – both for the configuration of the individual resources as well as their interaction. While each Solutions Construct implements a very small architectural pattern, they are designed so that multiple constructs can be combined by sharing a common resource. For instance, a Solutions Construct that implements an Amazon Simple Storage Service (Amazon S3) bucket invoking a Lambda function can be deployed with a second Solutions Construct that deploys a Lambda function that writes to an Amazon Simple Queue Service (Amazon SQS) queue by sharing the same Lambda function between the two constructs. You can compose complex architectures by connecting multiple Solutions Constructs together, as you will see in this example.

Visual representation of how AWS Solutions Constructs build abstractions upon the AWS CDK, which is then compiled into static CloudFormation templates.

Infrastructure as Code Abstraction Layers

In this article, you will build a robust, functional REST API based on an OpenAPI specification using the AWS CDK and AWS Solutions Constructs.

How it Works

This example is a microservice that saves and retrieves product orders. The behavior will be fully defined by an OpenAPI specification and will include the following methods:

Method Functionality Authorization
POST /order Accepts order attributes included in the request body.
Returns the orderId assigned to the new order.
AWS Identity and Access Management (IAM)
GET /order/$(orderId} Accepts an orderId as a path parameter.
Returns the fully populated order object.
IAM

The architecture implementing the service is shown in the diagram below. Each method will integrate with a Lambda function that implements the interactions with an Amazon DynamoDB table. The API will be protected by IAM authorization and all input and output data will be verified by API Gateway. All of this will be fully defined in an OpenAPI specification that is used to configure the REST API.

Displays how the aw-openapigateway-lambda and aws-lambda-dynamodb Solutions constructs combine to deploy the demo by sharing a Lambda function.

The Two Solutions Constructs Making up the Service Architecture

Infrastructure as code will be implemented with the AWS CDK and AWS Solutions Constructs. This example uses 2 Solutions Constructs:

aws-lambda-dynamodb – This construct “connects” a Lambda function and a DynamoDB table. This entails giving the Lambda function the minimum IAM privileges to read and write from the table and providing the DynamoDB table name to the Lambda function code with an environment variable. A Solutions Constructs pattern will create its resources based on best practices by default, but a client can also provide construct properties to override the default behaviors. A client can also choose not to have the pattern create a new resource by providing a resource that already exists.

aws-openapigateway-lambda – This construct deploys a REST API on API Gateway configured by the OpenAPI specification, integrating each method of the API with a Lambda function. The OpenAPI specification is stored as an asset in S3 and referenced by the CloudFormation template rather than embedded in the template. When the Lambda functions in the stack have been created, a custom resource processes the OpenAPI asset and updates all the method specifications with the arn of the associated Lambda function. An API can point to multiple Lambda functions, or a Lambda function can provide the implementation for multiple methods.

In this example you will create the aws-lambda-dynamodb construct first. This construct will create your Lambda function, which you then supply as an existing resource to the aws-openapigateway-lambda constructor. Sharing this function between the constructs will unite the two small patterns into a complete architecture.

Prerequisites

To deploy this example, you will need the following in your development environment:

  • Node.js 18.0.0 or later
  • Typescript 3.8 or later (npm -g install typescript)
  • AWS CDK 2.82.0 or later (npm install -g aws-cdk && cdk bootstrap)

The cdk bootstrap command will launch an S3 bucket and other resources that the CDK requires into your default region. You will need to bootstrap your account using a role with sufficient privileges – you may require an account administrator to complete that command.

Tip – While AWS CDK. 2.82.0 is the minimum required to make this example work, AWS recommends regularly updating your apps to use the latest CDK version.

To deploy the example stack, you will need to be running under an IAM role with the following privileges:

  • Create API Gateway APIs
  • Create IAM roles/policies
  • Create Lambda Functions
  • Create DynamoDB tables
  • GET/POST methods on API Gateway
  • AWSCloudFormationFullAccess (managed policy)

Build the App

  1. Somewhere on your workstation, create an empty folder named openapi-blog with these commands:

mkdir openapi-blog && cd openapi-blog

  1. Now create an empty CDK application using this command:

cdk init -l=typescript

  1. The application is going to be built using two Solutions Constructs, aws-openapigateway-lambda and aws-lambda-dynamodb. Install them in your application using these commands:

npm install @aws-solutions-constructs/aws-openapigateway-lambda

npm install @aws-solutions-constructs/aws-lambda-dynamodb

Tip – if you get an error along the lines of npm ERR! Could not resolve dependency and npm ERR! peer aws-cdk-lib@"^2.130.0", then you’ve installed a version of Solutions Constructs that depends on a newer version of the CDK. In package.json, update the aws-cdk-lib and aws-cdk dependencies to be the version in the peer error and run npm install. Now try the above npm install commands again.

The OpenAPI REST API specification will be in the api/openapi-blog.yml file. It defines the POST and GET methods, the format of incoming and outgoing data and the IAM Authorization for all HTTP calls.

  1. Create a folder named api under openapi-blog.
  2. Within the api folder, create a file called openapi-blog.yml with the following contents:
---
openapi: 3.0.2
info:
  title: openapi-blog example
  version: '1.0'
  description: 'defines an API with POST and GET methods for an order resource'
# x-amazon-* values are OpenAPI extensions to define API Gateway specific configurations
# This section sets up 2 types of validation and defines params-only validation
# as the default.
x-amazon-apigateway-request-validators:
  all:
    validateRequestBody: true
    validateRequestParameters: true
  params-only:
    validateRequestBody: false
    validateRequestParameters: true
x-amazon-apigateway-request-validator: params-only
paths:
  "/order":
    post:
      x-amazon-apigateway-auth:
        type: AWS_IAM
      x-amazon-apigateway-request-validator: all
      summary: Create a new order
      description: Create a new order
      x-amazon-apigateway-integration:
        httpMethod: POST
        # "OrderHandler" is a placeholder that aws-openapigateway-lambda will
        # replace with the Lambda function when it is available
        uri: OrderHandler
        passthroughBehavior: when_no_match
        type: aws_proxy
      requestBody:
        description: Create a new order
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/OrderAttributes"
        required: true
      responses:
        '200':
          description: Successful operation
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/OrderObject"
  "/order/{orderId}":
    get:
      x-amazon-apigateway-auth:
        type: AWS_IAM
      summary: Get Order by ID
      description: Returns order data for the provided ID
      x-amazon-apigateway-integration:
        httpMethod: POST
        # "OrderHandler" is a placeholder that aws-openapigateway-lambda will
        # replace with the Lambda function when it is available
        uri: OrderHandler
        passthroughBehavior: when_no_match
        type: aws_proxy
      parameters:
      - name: orderId
        in: path
        required: true
        schema:
          type: integer
          format: int64
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/OrderObject"
        '400':
          description: Bad order ID
        '404':
          description: Order ID not found
components:
  schemas:
    OrderAttributes:
      type: object
      additionalProperties: false
      required:
      - productId
      - quantity
      - customerId
      properties:
        productId:
          type: string
        quantity:
          type: integer
          format: int32
          example: 7
        customerId:
          type: string
    OrderObject:
      allOf:
      - "$ref": "#/components/schemas/OrderAttributes"
      - type: object
        additionalProperties: false
        required:
        - id
        properties:
          id:
            type: string

Most of the fields in this OpenAPI definition are explained in the OpenAPI specification, but the fields starting with x-amazon- are unique extensions for configuring API Gateway. In this case x-apigateway-auth values stipulate that the methods be protected with IAM authorization; the x-amazon-request-validator values tell the API to validate the request parameters by default and the parameters and request body when appropriate; and the x-amazon-apigateway-integration section defines the custom integration of the method with a Lambda function. When using the Solutions Construct, this field does not identify the specific Lambda function, but instead has a placeholder string (“OrderHandler”) that will be replaced with the correct function name during the launch.

While the API will accept and validate requests, you’ll need some business logic to actually implement the functionality. Let’s create a Lambda function with some rudimentary business logic:

  1. Create a folder structure lambda/order under openapi-blog.
  2. Within the order folder, create a file called index.js . Paste the code from this file into your index.js file.

Our Lambda function is very simple, consisting of some relatively generic SDK calls to Dynamodb. Depending upon the HTTP method passed in the event, it either creates a new order or retrieves (and returns) an existing order. Once the stack loads, you can check out the IAM role associated with the Lambda function and see that the construct also created a least privilege policy for accessing the table. When the code is written, the DynamoDB table name is not known, but the aws-lambda-dynamodb construct creates an environment variable with the table name that will do nicely:

// Excerpt from index.js
// Get the table name from the Environment Variable set by aws-lambda-dynamodb
const orderTableName = process.env.DDB_TABLE_NAME;

Now that the business logic and API definition are included in the project, it’s time to add the AWS CDK code that will launch the application resources. Since the API definition and your business logic are the differentiated aspects of your application, it would be ideal if the infrastructure to host your application could deployed with a minimal amount of code. This is where Solutions Constructs help – perform the following steps:

  1. Open the lib/openapi-blog-stack.ts file.
  2. Replace the contents with the following:
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { OpenApiGatewayToLambda } from '@aws-solutions-constructs/aws-openapigateway-lambda';
import { LambdaToDynamoDB } from '@aws-solutions-constructs/aws-lambda-dynamodb';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import * as path from 'path';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ddb from 'aws-cdk-lib/aws-dynamodb';

export class OpenapiBlogStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // This application is going to use a very simple DynamoDB table
    const simpleTableProps = {
      partitionKey: {
        name: "Id",
        type: ddb.AttributeType.STRING,
      },
      // Not appropriate for production, this setting is to ensure the demo can be easily removed
      removalPolicy: cdk.RemovalPolicy.DESTROY
    };

    // This Solutions Construct creates the Orders Lambda function 
    // and configures the IAM policy and environment variables "connecting" 
    // it to a new Dynamodb table
    const orderApparatus = new LambdaToDynamoDB(this, 'Orders', {
      lambdaFunctionProps: {
        runtime: lambda.Runtime.NODEJS_18_X,
        handler: 'index.handler',
        code: lambda.Code.fromAsset(`lambda/order`),
      },
      dynamoTableProps: simpleTableProps
    });

    // This Solutions Construct creates and configures the REST API,
    // integrating it with the new order Lambda function created by the
    // LambdaToDynamomDB construct above
    const newApi = new OpenApiGatewayToLambda(this, 'OpenApiGatewayToLambda', {
      // The OpenAPI is stored as an S3 asset where it can be accessed during the
      // CloudFormation Create Stack command
      apiDefinitionAsset: new Asset(this, 'ApiDefinitionAsset', {
        path: path.join(`api`, 'openapi-blog.yml')
      }),
      // The construct uses these records to integrate the methods in the OpenAPI spec
      // to Lambda functions in the CDK stack
      apiIntegrations: [
        {
          // These ids correspond to the placeholder values for uri in the OpenAPI spec
          id: 'OrderHandler',
          existingLambdaObj: orderApparatus.lambdaFunction
        }
      ]
    });

    // We output the URL of the resource for convenience here
    new cdk.CfnOutput(this, 'OrderUrl', {
      value: newApi.apiGateway.url + 'order',
    });
  }
}

Notice that the above code to create the infrastructure is only about two dozen lines. The constructs provide best practice defaults for all the resources they create, you just need to provide information unique to the use case (and any values that must override the defaults). For instance, while the LambdaToDynamoDB construct defines best practice default properties for the table, the client needs to provide at least the partition key. So that the demo cleans up completely when we’re done, there’s a removalPolicy property that instructs CloudFormation to delete the table when the stack is deleted. These minimal table properties and the location of the Lambda function code are all you need to provide to launch the LambdaToDynamoDB construct.

The OpenApiGatewayToLambda construct must be told where to find the OpenAPI specification and how to integrate with the Lambda function(s). The apiIntegrations property is a mapping of the placeholder strings used in the OpenAPI spec to the Lambda functions in the CDK stack. This code maps OrderHandler to the Lambda function created by the LambdaToDynamoDB construct. APIs integrating with more than one function can easily do this by creating more placeholder strings.

  1. Ensure all files are saved and build the application:

npm run build

  1. Launch the CDK stack:

cdk deploy

You may see some AWS_SOLUTIONS_CONSTRUCTS_WARNING:‘s here, you can safely ignore them in this case. The CDK will display any IAM changes before continuing – allowing you to review any IAM policies created in the stack before actually deploying. Enter ‘Y’ [Enter] to continue deploying the stack. When the deployment concludes successfully, you should see something similar to the following output:

...

OpenapiBlogStack: deploying... [1/1]

OpenapiBlogStack: creating CloudFormation changeset...
 
 ✅  OpenapiBlogStack
 
✨  Deployment time: 97.78s
 
Outputs:
OpenapiBlogStack.OpenApiGatewayToLambdaSpecRestApiEndpointD1FA5E3A = https://b73nx617gl.execute-api.us-east-1.amazonaws.com/prod/
OpenapiBlogStack.OrderUrl = https://b73nx617gl.execute-api.us-east-1.amazonaws.com/prod/order
Stack ARN:
arn:aws:cloudformation:us-east-1:123456789012:stack/OpenapiBlogStack/01df6970-dc05-11ee-a0eb-0a97cfc33817
 
✨  Total time: 100.07s

Test the App

Let’s test the new REST API using the API Gateway management console to confirm it’s working as expected. We’ll create a new order, then retrieve it.

  • Open the API Gateway management console and click on APIs in the left side menu
  • Find the new REST API in the list of APIs, it will begin with OpenApiGatewayToLambda and have a Created date of today. Click on it to open it.
  • On the Resources page that appears, click on POST under /order.
  • In the lower, right-hand panel, select the Test tab (if the Test tab is not shown, click the arrow to shift the displayed tabs).
  • The POST must include order data in the request body that matches the OrderAttributes schema defined by the OpenAPI spec. Enter the following data in the Request body field:

{
"productId": "prod234232",
"customerId": "cust203439",
"quantity": 5
}

  • Click the orange Test button at the bottom of the page.

The API Gateway console will display the results of the REST API call. Key things to look for are a Status of 200 and a Response Body resembling “{\"id\":\"ord1712062412777\"}" (this is the id of the new order created in the system, your value will differ).

You could go to the DynamoDB console to confirm that the new order exists in the table, but it will be more fun to check by querying the API. Use the GET method to confirm the new order was persisted:

  • Copy the id value from the Response body of the POST call – "{\"id\":\"ord1712062412777\"}"

Tip – select just the text between the \” patterns (don’t select the backslash or quotation marks).

  • Select the GET method under /{orderId} in the resource list. Paste the orderId you copied earlier into the orderId field under Path.
  • Click Test – this will execute the GET method and return the order you just created.

You should see a Status of 200 and a Response body with the full data from the Order you created in the previous step:

"{\"id\":\"ord1712062412777\",\"productId\":\"prod234232\",\"quantity\":\"5\",\"customerId\":\"cust203439\"}"

Let’s see how API Gateway is enforcing the inputs of the API. Let’s go back to the POST method and intentionally send an incorrect set of Order attributes.

  • Click on POST under /order
  • In the lower, right-hand panel, select the Test tab.
  • Enter the following data in the Request body field:

{
"productId": "prod234232",
"customerId": "cust203439",
"quality": 5
}

  • Click the orange Test button at the bottom of the page.

Now you should see an HTTP error status of 400, and a Response body of {"message": "Invalid request body"}.

Note that API Gateway caught the error, not any code in your Lambda function. In fact, the Lambda function was never invoked (you can take my word for it, or you can check for yourself on the Lambda management console).

Because you’re invoking the methods directly from the console, you are circumventing the IAM authorization. If you would like to test the API with an IAM authorized call from a client, this video includes excellent instruction on how to accomplish this from Postman.

Cleanup

To clean up the resources in the stack, run this command:

cdk destroy

In response to Are you sure you want to delete: OpenApiBlogStack (y/n)? you can type y (once again you can safely ignore the warnings here).

Conclusion

Defining your API in a standalone definition file decouples it from your implementation, provides documentation and client benefits, and leads to more clarity for all stakeholders. Using that definition to configure your REST API in API Gateway creates a robust API that offloads enforcement of the API from your business logic to your tooling.

Configuring a REST API that fully utilizes the functionality of API Gateway can be a daunting challenge. Defining the API behavior with an OpenAPI specification, then implementing that API using the AWS CDK and AWS Solutions Constructs, accelerates and simplifies that effort. The CloudFormation template that eventually launched this API is over 1200 lines long – yet with AWS CDK and AWS Solutions Constructs you were able generate this template with ~25 lines of Typescript.

This is just one example of how Solutions Constructs enable developers to rapidly produce high quality architectures with the AWS CDK. At this writing there are 72 Solutions Constructs covering 29 AWS services – take a moment to browse through what’s available on the Solutions Constructs site. Introducing these in your CDK stacks accelerates your development, jump starts your journey towards being well-architected, and helps keep you well-architected as best practices and technologies evolve in the future.

Picture of the author

About the Author

Biff Gaut has been shipping software since 1983, from small startups to large IT shops. Along the way he has contributed to 2 books, spoken at several conferences and written many blog posts. He’s been with AWS for 10+years and is currently a Principal Engineer working on the AWS Solutions Constructs team, helping customers deploy better architectures more quickly.

Rapid and flexible Infrastructure as Code using the AWS CDK with AWS Solutions Constructs

Post Syndicated from Biff Gaut original https://aws.amazon.com/blogs/devops/rapid-flexible-infrastructure-with-solutions-constructs-cdk/

Introduction

As workloads move to the cloud and all infrastructure becomes virtual, infrastructure as code (IaC) becomes essential to leverage the agility of this new world. JSON and YAML are the powerful, declarative modeling languages of AWS CloudFormation, allowing you to define complex architectures using IaC. Just as higher level languages like BASIC and C abstracted away the details of assembly language and made developers more productive, the AWS Cloud Development Kit (AWS CDK) provides a programming model above the native template languages, a model that makes developers more productive when creating IaC. When you instantiate CDK objects in your Typescript (or Python, Java, etc.) application, those objects “compile” into a YAML template that the CDK deploys as an AWS CloudFormation stack.

AWS Solutions Constructs take this simplification a step further by providing a library of common service patterns built on top of the CDK. These multi-service patterns allow you to deploy multiple resources with a single object, resources that follow best practices by default – both independently and throughout their interaction.

Comparison of an Application stack with Assembly Language, 4th generation language and Object libraries such as Hibernate with an IaC stack of CloudFormation, AWS CDK and AWS Solutions Constructs

Application Development Stack vs. IaC Development Stack

Solution overview

To demonstrate how using Solutions Constructs can accelerate the development of IaC, in this post you will create an architecture that ingests and stores sensor readings using Amazon Kinesis Data Streams, AWS Lambda, and Amazon DynamoDB.

An architecture diagram showing sensor readings being sent to a Kinesis data stream. A Lambda function will receive the Kinesis records and store them in a DynamoDB table.

Prerequisite – Setting up the CDK environment

Tip – If you want to try this example but are concerned about the impact of changing the tools or versions on your workstation, try running it on AWS Cloud9. An AWS Cloud9 environment is launched with an AWS Identity and Access Management (AWS IAM) role and doesn’t require configuring with an access key. It uses the current region as the default for all CDK infrastructure.

To prepare your workstation for CDK development, confirm the following:

  • Node.js 10.3.0 or later is installed on your workstation (regardless of the language used to write CDK apps).
  • You have configured credentials for your environment. If you’re running locally you can do this by configuring the AWS Command Line Interface (AWS CLI).
  • TypeScript 2.7 or later is installed globally (npm -g install typescript)

Before creating your CDK project, install the CDK toolkit using the following command:

npm install -g aws-cdk

Create the CDK project

  1. First create a project folder called stream-ingestion with these two commands:

mkdir stream-ingestion
cd stream-ingestion

  1. Now create your CDK application using this command:

npx [email protected] init app --language=typescript

Tip – This example will be written in TypeScript – you can also specify other languages for your projects.

At this time, you must use the same version of the CDK and Solutions Constructs. We’re using version 1.68.0 of both based upon what’s available at publication time, but you can update this with a later version for your projects in the future.

Let’s explore the files in the application this command created:

  • bin/stream-ingestion.ts – This is the module that launches the application. The key line of code is:

new StreamIngestionStack(app, 'StreamIngestionStack');

This creates the actual stack, and it’s in StreamIngestionStack that you will write the CDK code that defines the resources in your architecture.

  • lib/stream-ingestion-stack.ts – This is the important class. In the constructor of StreamIngestionStack you will add the constructs that will create your architecture.

During the deployment process, the CDK uploads your Lambda function to an Amazon S3 bucket so it can be incorporated into your stack.

  1. To create that S3 bucket and any other infrastructure the CDK requires, run this command:

cdk bootstrap

The CDK uses the same supporting infrastructure for all projects within a region, so you only need to run the bootstrap command once in any region in which you create CDK stacks.

  1. To install the required Solutions Constructs packages for our architecture, run the these two commands from the command line:

npm install @aws-solutions-constructs/[email protected]
npm install @aws-solutions-constructs/[email protected]

Write the code

First you will write the Lambda function that processes the Kinesis data stream messages.

  1. Create a folder named lambda under stream-ingestion
  2. Within the lambda folder save a file called lambdaFunction.js with the following contents:
var AWS = require("aws-sdk");

// Create the DynamoDB service object
var ddb = new AWS.DynamoDB({ apiVersion: "2012-08-10" });

AWS.config.update({ region: process.env.AWS_REGION });

// We will configure our construct to 
// look for the .handler function
exports.handler = async function (event) {
  try {
    // Kinesis will deliver records 
    // in batches, so we need to iterate through
    // each record in the batch
    for (let record of event.Records) {
      const reading = parsePayload(record.kinesis.data);
      await writeRecord(record.kinesis.partitionKey, reading);
    };
  } catch (err) {
    console.log(`Write failed, err:\n${JSON.stringify(err, null, 2)}`);
    throw err;
  }
  return;
};

// Write the provided sensor reading data to the DynamoDB table
async function writeRecord(partitionKey, reading) {

  var params = {
    // Notice that Constructs automatically sets up 
    // an environment variable with the table name.
    TableName: process.env.DDB_TABLE_NAME,
    Item: {
      partitionKey: { S: partitionKey },  // sensor Id
      timestamp: { S: reading.timestamp },
      value: { N: reading.value}
    },
  };

  // Call DynamoDB to add the item to the table
  await ddb.putItem(params).promise();
}

// Decode the payload and extract the sensor data from it
function parsePayload(payload) {

  const decodedPayload = Buffer.from(payload, "base64").toString(
    "ascii"
  );

  // Our CLI command will send the records to Kinesis
  // with the values delimited by '|'
  const payloadValues = decodedPayload.split("|", 2)
  return {
    value: payloadValues[0],
    timestamp: payloadValues[1]
  }
}

We won’t spend a lot of time explaining this function – it’s pretty straightforward and heavily commented. It receives an event with one or more sensor readings, and for each reading it extracts the pertinent data and saves it to the DynamoDB table.

You will use two Solutions Constructs to create your infrastructure:

The aws-kinesisstreams-lambda construct deploys an Amazon Kinesis data stream and a Lambda function.

  • aws-kinesisstreams-lambda creates the Kinesis data stream and Lambda function that subscribes to that stream. To support this, it also creates other resources, such as IAM roles and encryption keys.

The aws-lambda-dynamodb construct deploys a Lambda function and a DynamoDB table.

  • aws-lambda-dynamodb creates an Amazon DynamoDB table and a Lambda function with permission to access the table.
  1. To deploy the first of these two constructs, replace the code in lib/stream-ingestion-stack.ts with the following code:
import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import { KinesisStreamsToLambda } from "@aws-solutions-constructs/aws-kinesisstreams-lambda";

import * as ddb from "@aws-cdk/aws-dynamodb";
import { LambdaToDynamoDB } from "@aws-solutions-constructs/aws-lambda-dynamodb";

export class StreamIngestionStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const kinesisLambda = new KinesisStreamsToLambda(
      this,
      "KinesisLambdaConstruct",
      {
        lambdaFunctionProps: {
          // Where the CDK can find the lambda function code
          runtime: lambda.Runtime.NODEJS_10_X,
          handler: "lambdaFunction.handler",
          code: lambda.Code.fromAsset("lambda"),
        },
      }
    );

    // Next Solutions Construct goes here
  }
}

Let’s explore this code:

  • It instantiates a new KinesisStreamsToLambda object. This Solutions Construct will launch a new Kinesis data stream and a new Lambda function, setting up the Lambda function to receive all the messages in the Kinesis data stream. It will also deploy all the additional resources and policies required for the architecture to follow best practices.
  • The third argument to the constructor is the properties object, where you specify overrides of default values or any other information the construct needs. In this case you provide properties for the encapsulated Lambda function that informs the CDK where to find the code for the Lambda function that you stored as lambda/lambdaFunction.js earlier.
  1. Now you’ll add the second construct that connects the Lambda function to a new DynamoDB table. In the same lib/stream-ingestion-stack.ts file, replace the line // Next Solutions Construct goes here with the following code:
    // Define the primary key for the new DynamoDB table
    const primaryKeyAttribute: ddb.Attribute = {
      name: "partitionKey",
      type: ddb.AttributeType.STRING,
    };

    // Define the sort key for the new DynamoDB table
    const sortKeyAttribute: ddb.Attribute = {
      name: "timestamp",
      type: ddb.AttributeType.STRING,
    };

    const lambdaDynamoDB = new LambdaToDynamoDB(
      this,
      "LambdaDynamodbConstruct",
      {
        // Tell construct to use the Lambda function in
        // the first construct rather than deploy a new one
        existingLambdaObj: kinesisLambda.lambdaFunction,
        tablePermissions: "Write",
        dynamoTableProps: {
          partitionKey: primaryKeyAttribute,
          sortKey: sortKeyAttribute,
          billingMode: ddb.BillingMode.PROVISIONED,
          removalPolicy: cdk.RemovalPolicy.DESTROY
        },
      }
    );

    // Add autoscaling
    const readScaling = lambdaDynamoDB.dynamoTable.autoScaleReadCapacity({
      minCapacity: 1,
      maxCapacity: 50,
    });

    readScaling.scaleOnUtilization({
      targetUtilizationPercent: 50,
    });

Let’s explore this code:

  • The first two const objects define the names and types for the partition key and sort key of the DynamoDB table.
  • The LambdaToDynamoDB construct instantiated creates a new DynamoDB table and grants access to your Lambda function. The key to this call is the properties object you pass in the third argument.
    • The first property sent to LambdaToDynamoDB is existingLambdaObj – by setting this value to the Lambda function created by KinesisStreamsToLambda, you’re telling the construct to not create a new Lambda function, but to grant the Lambda function in the other Solutions Construct access to the DynamoDB table. This illustrates how you can chain many Solutions Constructs together to create complex architectures.
    • The second property sent to LambdaToDynamoDB tells the construct to limit the Lambda function’s access to the table to write only.
    • The third property sent to LambdaToDynamoDB is actually a full properties object defining the DynamoDB table. It provides the two attribute definitions you created earlier as well as the billing mode. It also sets the RemovalPolicy to DESTROY. This policy setting ensures that the table is deleted when you delete this stack – in most cases you should accept the default setting to protect your data.
  • The last two lines of code show how you can use statements to modify a construct outside the constructor. In this case we set up auto scaling on the new DynamoDB table, which we can access with the dynamoTable property on the construct we just instantiated.

That’s all it takes to create the all resources to deploy your architecture.

  1. Save all the files, then compile the Typescript into a CDK program using this command:

npm run build

  1. Finally, launch the stack using this command:

cdk deploy

(Enter “y” in response to Do you wish to deploy all these changes (y/n)?)

You will see some warnings where you override CDK default values. Because you are doing this intentionally you may disregard these, but it’s always a good idea to review these warnings when they occur.

Tip – Many mysterious CDK project errors stem from mismatched versions. If you get stuck on an inexplicable error, check package.json and confirm that all CDK and Solutions Constructs libraries have the same version number (with no leading caret ^). If necessary, correct the version numbers, delete the package-lock.json file and node_modules tree and run npm install. Think of this as the “turn it off and on again” first response to CDK errors.

You have now deployed the entire architecture for the demo – open the CloudFormation stack in the AWS Management Console and take a few minutes to explore all 12 resources that the program deployed (and the 380 line template generated to created them).

Feed the Stream

Now use the CLI to send some data through the stack.

Go to the Kinesis Data Streams console and copy the name of the data stream. Replace the stream name in the following command and run it from the command line.

aws kinesis put-records \
--stream-name StreamIngestionStack-KinesisLambdaConstructKinesisStreamXXXXXXXX-XXXXXXXXXXXX \
--records \
PartitionKey=1301,'Data=15.4|2020-08-22T01:16:36+00:00' \
PartitionKey=1503,'Data=39.1|2020-08-22T01:08:15+00:00'

Tip – If you are using the AWS CLI v2, the previous command will result in an “Invalid base64…” error because v2 expects the inputs to be Base64 encoded by default. Adding the argument --cli-binary-format raw-in-base64-out will fix the issue.

To confirm that the messages made it through the service, open the DynamoDB console – you should see the two records in the table.

Now that you’ve got it working, pause to think about what you just did. You deployed a system that can ingest and store sensor readings and scale to handle heavy loads. You did that by instantiating two objects – well under 60 lines of code. Experiment with changing some property values and deploying the changes by running npm run build and cdk deploy again.

Cleanup

To clean up the resources in the stack, run this command:

cdk destroy

Conclusion

Just as languages like BASIC and C allowed developers to write programs at a higher level of abstraction than assembly language, the AWS CDK and AWS Solutions Constructs allow us to create CloudFormation stacks in Typescript, Java, or Python instead JSON or YAML. Just as there will always be a place for assembly language, there will always be situations where we want to write CloudFormation templates manually – but for most situations, we can now use the AWS CDK and AWS Solutions Constructs to create complex and complete architectures in a fraction of the time with very little code.

AWS Solutions Constructs can currently be used in CDK applications written in Typescript, Javascript, Java and Python and will be available in C# applications soon.

About the Author

Biff Gaut has been shipping software since 1983, from small startups to large IT shops. Along the way he has contributed to 2 books, spoken at several conferences and written many blog posts. He is now a Principal Solutions Architect at AWS working on the AWS Solutions Constructs team, helping customers deploy better architectures more quickly.