[AWS] Build an App with AWS CDK

Install:

npm i -g aws-cdk

 

Init a project:

cdk init sample-app --language=typescript

 

To verify it works:

npm run build

Should have no error.

 

Developement:

npm run watch

To watch typescript files changes

 

See a preview of what you will be deploying:

cdk diff # similar to git diff

 Deploy: 

cdk deploy

 

CloudFormation Stack:

Cloud Development Kit is built on top of CloudFormation which is an AWS service that allows you to describe a stack in AWS using a static file (either YAML or JSON).

In essence - it‘s going to convert our code written in TypeScript, to JavaScript, which will be then converted to CloudFormation and CloudFormation will be used to deploy our infrastructure.

Sounds complicated, right? Luckily CDK abstracts a lot of things away from us, so we get to focus on solving our problems instead of writing YAML by hand.

 

Create a lambda function:

Install:

npm i --save @aws-cdk/asw-lambda @types/aws-lambda

 

in root folder, create a new folder ‘lambda‘, and create a hello.ts file:

// we are going to call this function via APIGatewayEvent which is used for http requests
exports.handler = async function (event: AWSLambda.APIGatewayEvent) {
  //  this is a neat trick to prettify the console log
  console.log("request:", JSON.stringify(event, null, 2));

  // this is what calling this lambda function will return
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    body: `Hello, egghead friends!`,
  };
};

 

Then in the main application file:

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
export class CdkDemoStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
  }

  helloLambda = new lambda.Function(this, "HelloLambdaKey", {
    // find code from lambda folder
    code: lambda.Code.fromAsset("lambda"),
    // hello.ts file: export handler function
    // therefore ‘hello.handler‘
    handler: "hello.handler",
    runtime: lambda.Runtime.NODEJS_12_X,
  });
}

 

Let‘s go to the AWS management console, search for Cloudformation, click on Stacks, and checkout what we‘ve deployed.

Under the Resources we can see:

  • AWS::IAM::Role
  • AWS::Lambda::Function
  • AWS::CDK::Metadata

Click on the lambda function id to explore it. You‘ll be able to use the entire code, which starts with use strict and finishes with the sourcemap (because this was code was transpiled from typescript).

Further down the page, you‘ll see Tags associated with this function (those were added automatically).

We currently don‘t have any Triggers so click on Test and Configure test event to test it manually. Choose Api Gateway AWS Proxy. Name the test then click Create.

Once you click Test again the lambda function will be called and you should see the console.log with "Hello, egghead friends!" (or your own text).

 

API Gateway:

Serverless technologies like AWS Lambda allow us to build our applications out of small, independent functions that can be called based on events such as an API call.

By default, it‘s not possible to call a lambda function from the Internet - it‘s safely stored within AWS cloud, following the principle of least privilege.

In order to create a REST API to call a lambda function we need to use API Gateway. Luckily, with AWS CDK, we can create a LambdaRestApi in 3 lines of code and this is exactly what we‘re going to learn in this quick lesson!

Run:

npm install --save @aws-cdk/aws-apigateway

Then import it to the stack file: 

import * as apiGateway from "@aws-cdk/aws-apigateway";

 

Now we will use apiGateway to create a REST API for our application.

import * as cdk from "@aws-cdk/core";
import * as lambda from "@aws-cdk/aws-lambda";
import * as apiGateway from "@aws-cdk/aws-apigateway";

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

        const helloLambda = new lambda.Function(this, "HelloLambda", {
            code: lambda.Code.fromAsset("lambda"),
            handler: "hello.handler",
            runtime: lambda.Runtime.NODEJS_12_X,
            memorySize: 256,
            timeout: cdk.Duration.seconds(10)
        });

        new apiGateway.LambdaRestApi(this, "Endpoint", {
            handler: helloLambda
        });
    }
}
//lambda/hello.ts

exports.handler = async function (event: AWSLambda.APIGatewayEvent) {
    console.log("event: ", JSON.stringify(event, null, 2));

    return {
        statusCode: 200,
        headers: { "Content-Type": "text/plain" },
        body: `Hello, egghead friends! You‘ve hit ${event.path}` // url
    };
};

Once you‘ve deployed succesfully, the terminal will output a URL. Click on it to see your lambda function live on the internet.

You can also check your newly created resourced in the aws console. If you click on your lambda function you‘ll also see that this function now has a trigger (API Gateway) associated with it.

 

Environment variable:

Environment variables are really useful in programming - they allow us to avoid hardcoding secrets (such as API keys) in our code with added bonus of using different values depending on the environment (hence the name!).

In this quick lesson we‘re going to learn how to pass environment variables to an AWS Lambda function deployed with Cloud Development Kit (in a single line of code!)

const helloLambda = new lambda.Function(this, "HelloLambda", {
    environment: { isProduction: "absolutely not" }
});

 

 

S3:

Install:

npm install --save @aws-cdk/aws-s3
import * as s3 from "@aws-cdk/aws-s3";

const logoBucket = new s3.Bucket(this, "LogoBucket", {
      publicReadAccess: true // make it public accessable
  });

 

Trigger a lambda function on file upload:

npm install --save @aws-cdk/aws-s3-notifications
import * as s3Notifications from @aws-cdk/aws-s3-notifications;

// attaching a notification to our logo bocket
logoBucket.addEventNotification(
      // everytime a new file is added to our bucket
      s3.EventType.OBJECT_CREATED,
      // execute our lambda function
      new s3Notifications.LambdaDestination(helloLambda)
  );

In the aws console, go to Services and search for s3. Upload a file to the bucket then check if the lambda function was triggered by going back to Services and looking for lambda.

?? You can see your recently accessed aws dashboards in the History sidebar on the left.

In your lambda dashboard notice how a new function was added (for me it was: TodoAppStack-BucketNotificationsHandler050a0587b75-1BQ2LOUD7KPXI).

Click on the HelloLambda function, then click Monitoring and View logs in CloudWatch.

Then click on the latest log and expand the event log (it will mention things like eventVersioneventSource etc) and look for the information about your recently uploaded image.

object": {
            "key": "Screenshot+2020-05-13+at+07.24.34.png",
            "size": 19145,
            "eTag": "40502d42d31dab5fe8581bd3d7ce0202",
            "sequencer": "005EBCD5882BF314F4"
        }

 

Upload a file to S3 automaticlly:

npm install --save @aws-cdk/aws-s3-deployment
import * as s3Deployment from "@aws-cdk/aws-s3-deployment";

new s3Deployment.BucketDeployment(this, "DeployLogo", {
        destinationBucket: logoBucket,
        // an array of sources
        sources: [s3Deployment.Source.asset("./assets")]
    });

 

 

Create a custom AWS CDK construct:

Now that we know a lot about shipping cloud resources with CDK it‘s time to start creating a serverless backend for our todo application.

We could add the database and new lambda functions to our main stack but after a while it might get difficult to maintain.

Instead, in this lesson we‘re going learn how to create our very own custom CDK construct

Learn more about CDK constructs here

[AWS] Build an App with AWS CDK

 

Create a new file next to our stack file (in the lib directory), called todo-backend.ts:

Import aws-cdk/core then, let‘s type our custom construct (which is going to look a lot like the logoBucket code from our stack file).

export class TodoBackend extends cdk.Construct {
  // so we can export it later
    public readonly handler: lambda.Function;

    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id);
    }
}

Then import the construct into our main stack app and create an instance of it:

import { TodoBackend } from "./todo-backend";

const todoBackend = new TodoBackend(this, "TodoBackend");

 

 

Add DynamoDB:

Install:

npm install --save @aws-cdk/aws-dynamodb

Import dynamodb to our backend then create a new dynamo db table.

export class TodoBackend extends cdk.Construct {
  // so we can export it later
    public readonly handler: lambda.Function;

    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id);
    }

    // save it as a const since we‘ll use it in a little bit
      const todosTable = new dynamodb.Table(this, "TodoTable", {
        //a unique key
            partitionKey: { name: "id", type: dynamodb.AttributeType.STRING }
        });

}

 

Get all items from DynamoDB table:

Create todoHandler.ts (we can also delete the other hello lambda function since we just used it for testing).

import AWS = require("aws-sdk");
// the table name that we get from an env variable
const tableName = process.env.TABLE_NAME || "";
// for interacting with dynamoDB from JavaScript / nodeJS
const dynamo = new AWS.DynamoDB.DocumentClient();

const createResponse = (
    body: string | AWS.DynamoDB.DocumentClient.ItemList,
    statusCode = 200
) => {
    return {
        statusCode,
        body: JSON.stringify(body, null, 2)
    };
};
// DynamoDB Scan operation scans and returns all of the items in the db
const getAllTodos = async () => {
    const scanResult = await dynamo
        .scan({
            TableName: tableName
        })
        .promise();

    return scanResult;
};
// async function that respons to apiGateway events
exports.handler = async function(event: AWSLambda.APIGatewayEvent) {
    try {
        const { httpMethod, body: requestBody } = event;
        // GET request
        if (httpMethod === "GET") {
            const response = await getAllTodos();

            return createResponse(response.Items || []);
        }
        return createResponse(
            `We only accept GET requests for now, not ${httpMethod}`,
            500
        );
    } catch (error) {
        console.log(error);
        return createResponse(error, 500);
    }
};

 

We‘ll have to make some changes to our todo-backend file. Let‘s make a new lambda function:

import * as cdk from "@aws-cdk/core";
import * as dynamodb from "@aws-cdk/aws-dynamodb";
import * as lambda from "@aws-cdk/aws-lambda";

export class TodoBackend extends cdk.Construct {
    public readonly handler: lambda.Function;

    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id);

        const todosTable = new dynamodb.Table(this, "TodoDatabase", {
            partitionKey: { name: "id", type: dynamodb.AttributeType.STRING }
        });

        this.handler = new lambda.Function(this, "TodoHandler", {
            code: lambda.Code.fromAsset("lambda"),
            handler: "todoHandler.handler",
            runtime: lambda.Runtime.NODEJS_12_X,
            environment: {
                TABLE_NAME: todosTable.tableName
            }
        });
// add scan right for the lambda function
todosTable.grantReadWriteData(this.handler); } }

 

And create and delete requests:

  
/// <reference types="aws-sdk" />
import AWS = require("aws-sdk");

const tableName = process.env.TABLE_NAME || "";
const dynamo = new AWS.DynamoDB.DocumentClient();

const createResponse = (
    body: string | AWS.DynamoDB.DocumentClient.ItemList,
    statusCode = 200
) => {
    return {
        statusCode,
        body: JSON.stringify(body, null, 2)
    };
};

const getAllTodos = async () => {
    const scanResult = await dynamo
        .scan({
            TableName: tableName
        })
        .promise();

    return scanResult;
};

const addTodoItem = async (data: { todo: string; id: string }) => {
    const { id, todo } = data;
    if (todo && todo !== "") {
        await dynamo
            .put({
                TableName: tableName,
                Item: {
                    id: "totally_random_id",
                    todo
                }
            })
            .promise();
    }

    return todo;
};

const deleteTodoItem = async (data: { id: string }) => {
    const { id } = data;

    if (id && id !== "") {
        await dynamo
            .delete({
                TableName: tableName,
                Key: {
                    id
                }
            })
            .promise();
    }

    return id;
};

exports.handler = async function (event: AWSLambda.APIGatewayEvent) {
    try {
        const { httpMethod, body: requestBody } = event;

        if (httpMethod === "GET") {
            const response = await getAllTodos();

            return createResponse(response.Items || []);
        }

        if (!requestBody) {
            return createResponse("Missing request body", 500);
        }

        const data = JSON.parse(requestBody);

        if (httpMethod === "POST") {
            const todo = await addTodoItem(data);
            return todo
                ? createResponse(`${todo} added to the database`)
                : createResponse("Todo is missing", 500);
        }

        if (httpMethod === "DELETE") {
            const id = await deleteTodoItem(data);
            return id
                ? createResponse(
                      `Todo item with an id of ${id} deleted from the database`
                  )
                : createResponse("ID is missing", 500);
        }

        return createResponse(
            `We only accept GET, POST, OPTIONS and DELETE, not ${httpMethod}`,
            500
        );
    } catch (error) {
        console.log(error);
        return createResponse(error, 500);
    }
};

 

Connect React app to a serverless backend deployed with CDK and fix CORS issues

Our serverless backend and infrastructure (that we both created and deployed ourselves with CDK!) is ready so we can connect it to our React app.

In this quick lesson we‘re going to learn how to connect the React app from the source code of this course to our API as well as how to add appropriate CORS headers to our API response.

 

In todoHandler.ts:

// update

const createResponse = (
    body: string | AWS.DynamoDB.DocumentClient.ItemList,
    statusCode = 200
) => {
    return {
        statusCode,
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "OPTIONS,GET,POST,DELETE"
        },
        body: JSON.stringify(body, null, 2)
    };
};

 

We need to support OPTIONS request inside handler as well:

exports.handler = async function (event: AWSLambda.APIGatewayEvent) {
    try {
        const { httpMethod, body: requestBody } = event;

        if (httpMethod === "OPTIONS") {
            return createResponse("OK");
        }

        if (httpMethod === "GET") {
            const response = await getAllTodos();

            return createResponse(response.Items || []);
        }

        if (!requestBody) {
            return createResponse("Missing request body", 500);
        }

        const data = JSON.parse(requestBody);

        if (httpMethod === "POST") {
            const todo = await addTodoItem(data);
            return todo
                ? createResponse(`${todo} added to the database`)
                : createResponse("Todo is missing", 500);
        }

        if (httpMethod === "DELETE") {
            const id = await deleteTodoItem(data);
            return id
                ? createResponse(
                      `Todo item with an id of ${id} deleted from the database`
                  )
                : createResponse("ID is missing", 500);
        }

        return createResponse(
            `We only accept GET, POST, OPTIONS and DELETE, not ${httpMethod}`,
            500
        );
    } catch (error) {
        console.log(error);
        return createResponse(error, 500);
    }
};

 

Add a custom CloudFormation stack output with CDK

Once we deploy static assets to an S3 bucket it‘s good to know, well, where are they exactly - saying that "they‘re in the cloud" is not a good answer.

We can find the S3 bucket and other resources in AWS Console manually but there‘s a better solution - creating custom CloudFormation stack outputs that are displayed in the terminal once we deploy.

In this quick lesson we‘re going to learn how to create a custom CloudFormation stack output in order to output the path to an egghead logo in order to add it to our app.

 

Let‘s fix the missing logo at the bottom of our todo application. We need to tell our frontend application to source it from our s3 logoBucket.

Instead, searching for the logo url in our aws console (not a true hacker move!, we can output the url in our terminal with each deployment.

To do that, let‘s modify the output in our stack file by adding this:

In stack:

  new cdk.CfnOutput(this, "LogoPath", {
    // add the name of your bucket and your file (in the assets folder)
      value: `https://${logoBucket.bucketDomainName}/testFile.png`
  });

Once you deploy, you should see the logo path in the outputs section.

 

Deploy a site with HTTPS support behind a CDN with CDK

Deploying a static site to S3 works really well unless we consider the fact that you cannot add HTTPS support to the site directly in S3. In order to do that - a CloudFront distribution is required.

While it is possible to setup CloudFront with CDK on our own, we don‘t always have to do anything on our own.

Instead, we can use constructs that were created by the community.

In this quick lesson we‘re going to learn how to use CDK-SPA-Deploy construct in order to deploy our app to S3 (using createBasicSite construct) and deploy it with HTTPS support and behind a CloudFront CDN distribution.

Install:

npm install --save cdk-spa-deploy

 

Import it to the stack file:

import { SPADeploy } from "cdk-spa-deploy";

Add:

new SPADeploy(this, "WebsiteDeployment").createSiteWithCloudfront({
    indexDoc: "index.html",
    websiteFolder: "../frontend/build"
});

 

Full stack code:

import * as cdk from "@aws-cdk/core";
import * as apiGateway from "@aws-cdk/aws-apigateway";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3Deployment from "@aws-cdk/aws-s3-deployment";
import { TodoDatabase } from "./todo-database";
import { SPADeploy } from "cdk-spa-deploy";

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

        const todoDatabase = new TodoDatabase(this, "TodoDatabase");

        new apiGateway.LambdaRestApi(this, "Endpoint", {
            handler: todoDatabase.handler
        });

        const logoBucket = new s3.Bucket(this, "LogoBucket", {
            publicReadAccess: true
        });

        new s3Deployment.BucketDeployment(this, "DeployLogo", {
            destinationBucket: logoBucket,
            sources: [s3Deployment.Source.asset("./assets")]
        });

        new SPADeploy(this, "WebsiteDeployment").createSiteWithCloudfront({ 
       indexDoc: "index.html",
       websiteFolder: "../frontend/build"
     });
} }

  

[AWS] Build an App with AWS CDK

上一篇:appium进行元素识别的操作过程


下一篇:安卓四大组件之服务(1)服务的生命周期和启动方式