Amazon API Gateway to AWS Lambda to Amazon QLDB

Sign Up to Build

About this Architecture

Here is some information about this architecture.

How to Build This Solution

Here are the steps you can follow to build this solution on your own.

This project involves creating a serverless architecture utilizing Amazon API Gateway, AWS Lambda, and Amazon Quantum Ledger Database (QLDB). You'll learn how to set up API Gateway to trigger Lambda functions, process data, and store the results in Amazon QLDB. This lesson showcases how to implement a robust, fully-managed ledger database that provides transparent, immutable, and cryptographically verifiable transactions.

Get Your AWS Credentials

If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.

Wait for the credentials to load. Then run this in the terminal.

Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.

$ aws configure --profile smx-lab
AWS Access Key ID [None]: 
AWS Secret Access Key [None]: 
Default region name [None]: us-west-2
Default output format [None]: 

Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.

Create the Terraform Config

Next, we'll create the required_providers config. This config is used to specify the required providers for our Terraform project. In this case, we are specifying that we need the aws provider from HashiCorp with a version constraint of ~> 4.22.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.22"
    }
  }
  required_version = ">= 0.14.9"
}

Create the aws Provider

Next, we'll create the aws config which is used to configure the AWS provider in Terraform.

provider "aws" {
  profile = "default"
  region  = "us-east-1"
}

Create the aws_iam_policy Data Config

Next, we'll create the aws_iam_policy config. This config is used to define an IAM policy for the lambda_basic_execution_role_policy with the name AWSLambdaBasicExecutionRole.

data "aws_iam_policy" "lambda_basic_execution_role_policy" {
  name = "AWSLambdaBasicExecutionRole"
}

Create the archive_file Data Config

Next, we'll create the archive_file config. This config is used to create a zip file from the source directory specified, which in this case is ${path.module}/src/. The output of this config will be stored in ${path.module}/lambda.zip.

data "archive_file" "lambda_zip_file" {
  type        = "zip"
  source_dir  = "${path.module}/src/"
  output_path = "${path.module}/lambda.zip"
}

Create the aws_iam_policy_document Data Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document for a Lambda function.

data "aws_iam_policy_document" "lambda_policy_document_create_person" {
  statement {
    effect   = "Allow"
    actions  = ["qldb:SendCommand"]
    resources = ["${aws_qldb_ledger.ledger.arn}"]
  }

  statement {
    effect   = "Allow"
    actions  = ["qldb:PartiQLInsert", "qldb:PartiQLUpdate", "qldb:PartiQLSelect"]
    resources = ["${aws_qldb_ledger.ledger.arn}/*"]
  }
}

Create the aws_iam_policy_document Data Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document for an AWS Lambda function.

data "aws_iam_policy_document" "lambda_policy_document_get_person" {
  statement {
    effect   = "Allow"
    actions  = ["qldb:SendCommand"]
    resources = ["${aws_qldb_ledger.ledger.arn}"]
  }

  statement {
    effect   = "Allow"
    actions  = ["qldb:PartiQLSelect"]
    resources = ["${aws_qldb_ledger.ledger.arn}/*"]
  }
}

Create the aws_iam_policy_document Data Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document for an AWS Lambda function.

data "aws_iam_policy_document" "lambda_policy_document_get_person_history" {
  statement {
    effect   = "Allow"
    actions  = ["qldb:SendCommand"]
    resources = ["${aws_qldb_ledger.ledger.arn}"]
  }

  statement {
    effect   = "Allow"
    actions  = ["qldb:PartiQLHistoryFunction"]
    resources = ["${aws_qldb_ledger.ledger.arn}/*"]
  }
}

Create the aws_iam_policy_document Data Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document for an AWS Lambda function.

data "aws_iam_policy_document" "lambda_policy_document_update_person" {
  statement {
    effect   = "Allow"
    actions  = ["qldb:SendCommand"]
    resources = ["${aws_qldb_ledger.ledger.arn}"]
  }

  statement {
    effect   = "Allow"
    actions  = ["qldb:PartiQLSelect", "qldb:PartiQLUpdate"]
    resources = ["${aws_qldb_ledger.ledger.arn}/*"]
  }
}

Create the aws_iam_policy_document Data Config

Next, we'll create the aws_iam_policy_document config. This config is used to define an IAM policy document for a Lambda function.

data "aws_iam_policy_document" "lambda_policy_document_delete_person" {
  statement {
    effect   = "Allow"
    actions  = ["qldb:SendCommand"]
    resources = ["${aws_qldb_ledger.ledger.arn}"]
  }

  statement {
    effect   = "Allow"
    actions  = ["qldb:PartiQLDelete", "qldb:PartiQLSelect"]
    resources = ["${aws_qldb_ledger.ledger.arn}/*"]
  }
}

Create the aws_qldb_ledger Resource

Next, we'll create the aws_qldb_ledger config. This config is used to create an Amazon QLDB ledger with the following properties:

  • name: The name of the QLDB ledger, set to qldb-serverless-pattern.

  • permissions_mode: The permissions mode for the ledger, set to STANDARD.

  • deletion_protection: Whether deletion protection is enabled for the ledger, set to False.

  • tags: Additional tags for the ledger, with a single tag named name set to qldb-serverless-pattern.

resource "aws_qldb_ledger" "ledger" {
  name                  = "qldb-serverless-pattern"
  permissions_mode      = "STANDARD"
  deletion_protection   = false

  tags = {
    name = "qldb-serverless-pattern"
  }
}

Create the aws_lambda_function Resource

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function named lambda_function_create_person.

resource "aws_lambda_function" "lambda_function_create_person" {
  function_name    = "CreatePerson"
  filename         = "${data.archive_file.lambda_zip_file.output_path}"
  source_code_hash = "${data.archive_file.lambda_zip_file.output_base64sha256}"
  handler          = "create-person.handler"
  role             = "${aws_iam_role.lambda_iam_role_create_person.arn}"
  runtime          = "nodejs14.x"

  environment {
    variables = {
      LEDGER_NAME                     = "${aws_qldb_ledger.ledger.id}"
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
    }
  }
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an IAM role in AWS.

resource "aws_iam_role" "lambda_iam_role_create_person" {
  name_prefix           = "LambdaCreatePersonRole-"
  managed_policy_arns   = ["${data.aws_iam_policy.lambda_basic_execution_role_policy.arn}", "${aws_iam_policy.lambda_policy_create_person.arn}"]
  assume_role_policy    = <<EOF
  {
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": "sts:AssumeRole",
        "Principal": {
          "Service": "lambda.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
      }
    ]
  }
  EOF
}

This configuration block creates an IAM role named lambda_iam_role_create_person with a name prefix of LambdaCreatePersonRole-. It also attaches two managed policies, lambda_basic_execution_role_policy and lambda_policy_create_person, to the role. The assume_role_policy specifies that the role can be assumed by the AWS Lambda service.

Note: The data.aws_iam_policy.lambda_basic_execution_role_policy.arn and aws_iam_policy.lambda_policy_create_person.arn references in the managed_policy_arns attribute should be replaced with the actual ARNs of the policies you want to attach.

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to create an IAM policy in AWS.

resource "aws_iam_policy" "lambda_policy_create_person" {
  name_prefix = "lambda_policy_create_person-"
  path        = "/"
  policy      = "${data.aws_iam_policy_document.lambda_policy_document_create_person.json}"
}

Create the aws_lambda_function Resource

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function named lambda_function_get_person.

The function is defined with the following properties:

- function_name: The name of the Lambda function is set to GetPerson.

- filename: The filename of the Lambda function's source code is set to the output path of the lambda_zip_file archive file.

- source_code_hash: The base64-encoded SHA256 hash of the lambda_zip_file archive file is set as the source code hash.

- handler: The handler function for the Lambda function is set to get-person.handler.

- role: The ARN of the IAM role lambda_iam_role_get_person is set as the execution role for the Lambda function.

- runtime: The runtime environment for the Lambda function is set to nodejs14.x.

- memory_size: The memory size allocated to the Lambda function is set to 512 MB. - environment: The environment variables for the Lambda function are set with an array containing a single object. This object defines two variables: LEDGER_NAME with the value of the id of the aws_qldb_ledger.ledger resource, and AWS_NODEJS_CONNECTION_REUSE_ENABLED with the value of 1.

resource "aws_lambda_function" "lambda_function_get_person" {
  function_name    = "GetPerson"
  filename         = "${data.archive_file.lambda_zip_file.output_path}"
  source_code_hash = "${data.archive_file.lambda_zip_file.output_base64sha256}"
  handler          = "get-person.handler"
  role             = "${aws_iam_role.lambda_iam_role_get_person.arn}"
  runtime          = "nodejs14.x"
  memory_size      = 512

  environment {
    variables = {
      LEDGER_NAME                     = "${aws_qldb_ledger.ledger.id}"
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
    }
  }
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an IAM role in AWS. In this case, we are creating a role named lambda_iam_role_get_person with a name prefix of LambdaGetPersonRole-. The role will have two managed policies attached, lambda_basic_execution_role_policy and lambda_policy_get_person. The assume_role_policy specifies that the role can be assumed by the lambda.amazonaws.com service.

resource "aws_iam_role" "lambda_iam_role_get_person" {
  name_prefix   = "LambdaGetPersonRole-"
  managed_policy_arns = [
    "${data.aws_iam_policy.lambda_basic_execution_role_policy.arn}",
    "${aws_iam_policy.lambda_policy_get_person.arn}"
  ]
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to define an IAM policy in AWS. In this case, we are creating a policy named lambda_policy_get_person with a name prefix of lambda_policy_get_person-. The policy will be created at the root level (/) and will use the contents of the lambda_policy_document_get_person.json file as the policy document.

resource "aws_iam_policy" "lambda_policy_get_person" {
  name_prefix = "lambda_policy_get_person-"
  path        = "/"
  policy      = "${data.aws_iam_policy_document.lambda_policy_document_get_person.json}"
}

Create the aws_lambda_function Resource

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function named lambda_function_get_person_history.

resource "aws_lambda_function" "lambda_function_get_person_history" {
  function_name    = "GetPersonHistory"
  filename         = "${data.archive_file.lambda_zip_file.output_path}"
  source_code_hash = "${data.archive_file.lambda_zip_file.output_base64sha256}"
  handler          = "get-person-history.handler"
  role             = "${aws_iam_role.lambda_iam_role_get_person_history.arn}"
  runtime          = "nodejs14.x"
  memory_size      = 512

  environment {
    variables = {
      LEDGER_NAME                    = "${aws_qldb_ledger.ledger.id}"
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
    }
  }
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an IAM role in AWS. In this case, we are creating a role named lambda_iam_role_get_person_history with a name prefix of LambdaGetPersonHistoryRole-. The role will have two managed policies attached to it, lambda_basic_execution_role_policy and lambda_policy_get_person_history. The assume_role_policy specifies that the role can be assumed by the lambda.amazonaws.com service.

resource "aws_iam_role" "lambda_iam_role_get_person_history" {
  name_prefix   = "LambdaGetPersonHistoryRole-"
  managed_policy_arns = [
    "${data.aws_iam_policy.lambda_basic_execution_role_policy.arn}",
    "${aws_iam_policy.lambda_policy_get_person_history.arn}"
  ]
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

Remember to replace any variables or references with their actual values before applying this configuration.

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to define an IAM policy in AWS. In this case, we are creating a policy named lambda_policy_get_person_history with a name prefix of lambda_policy_get_person_history-. The policy will be created at the root path (/) and the policy document will be sourced from the lambda_policy_document_get_person_history.json file.

resource "aws_iam_policy" "lambda_policy_get_person_history" {
  name_prefix = "lambda_policy_get_person_history-"
  path        = "/"
  policy      = "${data.aws_iam_policy_document.lambda_policy_document_get_person_history.json}"
}

Create the aws_lambda_function Resource

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function named lambda_function_update_person.

resource "aws_lambda_function" "lambda_function_update_person" {
  function_name    = "UpdatePerson"
  filename         = "${data.archive_file.lambda_zip_file.output_path}"
  source_code_hash = "${data.archive_file.lambda_zip_file.output_base64sha256}"
  handler          = "update-person.handler"
  role             = "${aws_iam_role.lambda_iam_role_update_person.arn}"
  runtime          = "nodejs14.x"
  memory_size      = 512

  environment {
    variables = {
      LEDGER_NAME                     = "${aws_qldb_ledger.ledger.id}"
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
    }
  }
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an AWS IAM role for a Lambda function. The role will have a name prefix of 'LambdaUpdatePersonRole-' and will be associated with two managed policies: 'lambda_basic_execution_role_policy' and 'lambda_policy_update_person'. The assume role policy allows the Lambda function to assume this role.

resource "aws_iam_role" "lambda_iam_role_update_person" {
  name_prefix   = "LambdaUpdatePersonRole-"
  managed_policy_arns = [
    "${data.aws_iam_policy.lambda_basic_execution_role_policy.arn}",
    "${aws_iam_policy.lambda_policy_update_person.arn}"
  ]
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to define an IAM policy in AWS. In this case, we are creating a policy named lambda_policy_update_person with a name prefix of lambda_policy_update_person-. The policy will be created at the root path (/) and will use the contents of the lambda_policy_document_update_person.json file as the policy document.

resource "aws_iam_policy" "lambda_policy_update_person" {
  name_prefix = "lambda_policy_update_person-"
  path        = "/"
  policy      = "${data.aws_iam_policy_document.lambda_policy_document_update_person.json}"
}

Create the aws_lambda_function Resource

Next, we'll create the aws_lambda_function config. This config is used to define an AWS Lambda function named lambda_function_delete_person.

resource "aws_lambda_function" "lambda_function_delete_person" {
  function_name    = "DeletePerson"
  filename         = "${data.archive_file.lambda_zip_file.output_path}"
  source_code_hash = "${data.archive_file.lambda_zip_file.output_base64sha256}"
  handler          = "delete-person.handler"
  role             = "${aws_iam_role.lambda_iam_role_delete_person.arn}"
  runtime          = "nodejs14.x"
  memory_size      = 512

  environment {
    variables = {
      LEDGER_NAME                     = "${aws_qldb_ledger.ledger.id}"
      AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
    }
  }
}

Create the aws_iam_role Resource

Next, we'll create the aws_iam_role config. This config is used to create an IAM role in AWS. In this case, we are creating a role named lambda_iam_role_delete_person with a name prefix of LambdaDeletePersonRole-. The role will have two managed policies attached, lambda_basic_execution_role_policy and lambda_policy_delete_person. The assume_role_policy specifies that the role can be assumed by the lambda.amazonaws.com service.

resource "aws_iam_role" "lambda_iam_role_delete_person" {
  name_prefix   = "LambdaDeletePersonRole-"
  managed_policy_arns = [
    "${data.aws_iam_policy.lambda_basic_execution_role_policy.arn}",
    "${aws_iam_policy.lambda_policy_delete_person.arn}"
  ]
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

Create the aws_iam_policy Resource

Next, we'll create the aws_iam_policy config. This config is used to define an IAM policy in AWS. In this example, we are creating a policy named lambda_policy_delete_person with a name prefix of lambda_policy_delete_person-. The policy is set to be created at the root path (/) and its contents are defined by the lambda_policy_document_delete_person.json file.

resource "aws_iam_policy" "lambda_policy_delete_person" {
  name_prefix = "lambda_policy_delete_person-"
  path        = "/"
  policy      = "${data.aws_iam_policy_document.lambda_policy_document_delete_person.json}"
}

Create the aws_api_gateway_rest_api Resource

Next, we'll create the aws_api_gateway_rest_api config. This config is used to create an API Gateway REST API in AWS.

resource "aws_api_gateway_rest_api" "api" {
  name = "qldb-api"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

Create the aws_api_gateway_resource Resource

Next, we'll create the aws_api_gateway_resource config. This config is used to define a resource in the AWS API Gateway. In this example, we are creating a resource named "person" with a path part of "person". The parent_id parameter specifies the parent resource ID, which is the root resource of the API. The rest_api_id parameter specifies the ID of the API to which this resource belongs.

resource "aws_api_gateway_resource" "person" {
  parent_id   = "${aws_api_gateway_rest_api.api.root_resource_id}"
  path_part   = "person"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_resource Resource

Next, we'll create the aws_api_gateway_resource config. This config is used to define a resource in the AWS API Gateway. In this example, we are creating a resource with the name personid under the parent resource person. The path_part specifies the URL path for this resource, and the rest_api_id specifies the ID of the API to which this resource belongs.

resource "aws_api_gateway_resource" "personid" {
  parent_id   = "${aws_api_gateway_resource.person.id}"
  path_part   = "{personid}"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_resource Resource

Next, we'll create the aws_api_gateway_resource config. This config is used to define a resource in the AWS API Gateway. In this case, we are creating a resource called person_history with a path_part of history. The parent_id is set to the id of another resource called person, and the rest_api_id is set to the id of an API called api.

resource "aws_api_gateway_resource" "person_history" {
  parent_id   = "${aws_api_gateway_resource.person.id}"
  path_part   = "history"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_resource Resource

Next, we'll create the aws_api_gateway_resource config. This config is used to define a resource in the AWS API Gateway. In this specific example, we are creating a resource called person_history_personid with the following properties:

  • parent_id: This property specifies the ID of the parent resource that this resource will be nested under. It is set to ${aws_api_gateway_resource.person_history.id}, which means it will be nested under the person_history resource.

  • path_part: This property specifies the path part of the resource's URL. In this case, it is set to {personid}, which means that the resource will be accessible at a URL like /person_history/{personid}.

  • rest_api_id: This property specifies the ID of the REST API that this resource belongs to. It is set to ${aws_api_gateway_rest_api.api.id}, which means it belongs to the api REST API.

Here is the HCL code for creating the aws_api_gateway_resource config:

resource "aws_api_gateway_resource" "person_history_personid" {
  parent_id    = "${aws_api_gateway_resource.person_history.id}"
  path_part    = "{personid}"
  rest_api_id  = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_method Resource

Next, we'll create the aws_api_gateway_method config. This config is used to define the method for the API Gateway resource. In this case, we are creating a POST method with no authorization.

resource "aws_api_gateway_method" "person_post" {
  authorization = "NONE"
  http_method   = "POST"
  resource_id   = "${aws_api_gateway_resource.person.id}"
  rest_api_id   = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_integration Resource

Next, we'll create the aws_api_gateway_integration config. This config is used to define the integration between the API Gateway and a Lambda function. In this case, it is specifically used for the 'person_post' API Gateway integration.

resource "aws_api_gateway_integration" "person_post" {
  http_method        = "${aws_api_gateway_method.person_post.http_method}"
  resource_id        = "${aws_api_gateway_resource.person.id}"
  rest_api_id        = "${aws_api_gateway_rest_api.api.id}"
  type               = "AWS_PROXY"
  uri                = "${aws_lambda_function.lambda_function_create_person.invoke_arn}"
  integration_http_method = "POST"
}

Create the aws_lambda_permission Resource

Next, we'll create the aws_lambda_permission config. This config is used to grant permission for the API Gateway to invoke the Lambda function lambda_function_create_person.

resource "aws_lambda_permission" "apigw_lambda_function_create_person" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda_function_create_person.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.person_post.http_method}${aws_api_gateway_resource.person.path}"
}

Create the aws_api_gateway_method Resource

Next, we'll create the aws_api_gateway_method config. This config is used to define a method for the AWS API Gateway. In this example, we are creating a GET method with no authorization for the personid resource.

resource "aws_api_gateway_method" "personid_get" {
  authorization   = "NONE"
  http_method     = "GET"
  resource_id     = "${aws_api_gateway_resource.personid.id}"
  rest_api_id     = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_integration Resource

Next, we'll create the aws_api_gateway_integration config. This config is used to define the integration between the API Gateway and a Lambda function. It specifies the HTTP method, resource ID, REST API ID, integration type, URI, and integration HTTP method.

resource "aws_api_gateway_integration" "personid_get" {
  http_method           = "${aws_api_gateway_method.personid_get.http_method}"
  resource_id           = "${aws_api_gateway_resource.personid.id}"
  rest_api_id           = "${aws_api_gateway_rest_api.api.id}"
  type                  = "AWS_PROXY"
  uri                   = "${aws_lambda_function.lambda_function_get_person.invoke_arn}"
  integration_http_method = "POST"
}

Create the aws_lambda_permission Config

Next, we'll create the aws_lambda_permission config. This config is used to grant permission for the API Gateway to invoke the Lambda function.

resource "aws_lambda_permission" "apigw_lambda_function_get_person" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda_function_get_person.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.personid_get.http_method}${aws_api_gateway_resource.personid.path}"
}

Create the aws_api_gateway_method Resource

Next, we'll create the aws_api_gateway_method config. This config is used to define a method for the AWS API Gateway. In this example, we are creating a GET method with no authorization for the person_history_personid_get resource.

resource "aws_api_gateway_method" "person_history_personid_get" {
  authorization   = "NONE"
  http_method     = "GET"
  resource_id     = "${aws_api_gateway_resource.person_history_personid.id}"
  rest_api_id     = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_integration Resource

Next, we'll create the aws_api_gateway_integration config. This config is used to define the integration between the API Gateway and a Lambda function. It specifies the HTTP method, resource ID, REST API ID, integration type, URI, and integration HTTP method.

resource "aws_api_gateway_integration" "person_history_personid_get" {
  http_method           = "${aws_api_gateway_method.person_history_personid_get.http_method}"
  resource_id           = "${aws_api_gateway_resource.person_history_personid.id}"
  rest_api_id           = "${aws_api_gateway_rest_api.api.id}"
  type                  = "AWS_PROXY"
  uri                   = "${aws_lambda_function.lambda_function_get_person_history.invoke_arn}"
  integration_http_method = "POST"
}

Create the aws_lambda_permission Resource

Next, we'll create the aws_lambda_permission config. This config is used to grant permission for the API Gateway to invoke the Lambda function apigw_lambda_function_get_person_history.

resource "aws_lambda_permission" "apigw_lambda_function_get_person_history" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda_function_get_person_history.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.person_history_personid_get.http_method}${aws_api_gateway_resource.person_history_personid.path}"
}

Create the aws_api_gateway_method Resource

Next, we'll create the aws_api_gateway_method config. This config is used to define the method for the API Gateway resource. In this case, we are creating a POST method with no authorization.

resource "aws_api_gateway_method" "personid_post" {
  authorization   = "NONE"
  http_method     = "POST"
  resource_id     = "${aws_api_gateway_resource.personid.id}"
  rest_api_id     = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_integration Resource

Next, we'll create the aws_api_gateway_integration config. This config is used to define the integration between the API Gateway and a Lambda function. It specifies the HTTP method, resource ID, REST API ID, integration type, URI, and integration HTTP method.

resource "aws_api_gateway_integration" "update_person_post" {
  http_method           = "${aws_api_gateway_method.personid_post.http_method}"
  resource_id           = "${aws_api_gateway_resource.personid.id}"
  rest_api_id           = "${aws_api_gateway_rest_api.api.id}"
  type                  = "AWS_PROXY"
  uri                   = "${aws_lambda_function.lambda_function_update_person.invoke_arn}"
  integration_http_method = "POST"
}

Create the aws_lambda_permission Resource

Next, we'll create the aws_lambda_permission config. This config is used to grant permission for the API Gateway to invoke the Lambda function apigw_lambda_function_update_person.

resource "aws_lambda_permission" "apigw_lambda_function_update_person" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda_function_update_person.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.personid_post.http_method}${aws_api_gateway_resource.personid.path}"
}

Create the aws_api_gateway_method Resource

Next, we'll create the aws_api_gateway_method config. This config is used to define a method for the AWS API Gateway. In this example, we are creating a DELETE method with no authorization. The resource_id and rest_api_id are set using variables.

resource "aws_api_gateway_method" "personid_delete" {
  authorization = "NONE"
  http_method   = "DELETE"
  resource_id   = "${aws_api_gateway_resource.personid.id}"
  rest_api_id   = "${aws_api_gateway_rest_api.api.id}"
}

Create the aws_api_gateway_integration Resource

Next, we'll create the aws_api_gateway_integration config. This config is used to define the integration between the API Gateway and a Lambda function for the person_delete resource. It specifies the HTTP method, resource ID, REST API ID, integration type, URI, and integration HTTP method.

resource "aws_api_gateway_integration" "person_delete" {
  http_method           = "${aws_api_gateway_method.personid_delete.http_method}"
  resource_id           = "${aws_api_gateway_resource.personid.id}"
  rest_api_id           = "${aws_api_gateway_rest_api.api.id}"
  type                  = "AWS_PROXY"
  uri                   = "${aws_lambda_function.lambda_function_delete_person.invoke_arn}"
  integration_http_method = "POST"
}

Create the aws_lambda_permission Resource

Next, we'll create the aws_lambda_permission config. This config is used to grant permission for the API Gateway to invoke the lambda_function_delete_person Lambda function.

resource "aws_lambda_permission" "apigw_lambda_function_delete_person" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.lambda_function_delete_person.function_name}"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/${aws_api_gateway_method.personid_delete.http_method}${aws_api_gateway_resource.personid.path}"
}

Create the aws_api_gateway_deployment Resource

Next, we'll create the aws_api_gateway_deployment config. This config is used to deploy an API Gateway REST API.

resource "aws_api_gateway_deployment" "api" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  triggers = {
    redeployment = "${sha1(jsonencode(aws_api_gateway_rest_api.api.body))}"
  }
  lifecycle {
    create_before_destroy = true
  }
}

Create the aws_api_gateway_stage Resource

Next, we'll create the aws_api_gateway_stage config. This config is used to define the stage settings for an API Gateway deployment. In this example, we are creating a stage named "Prod" with a deployment ID and a REST API ID.

resource "aws_api_gateway_stage" "prod" {
  deployment_id = "${aws_api_gateway_deployment.api.id}"
  rest_api_id   = "${aws_api_gateway_rest_api.api.id}"
  stage_name    = "Prod"
}

Create the PersonLedger Output

Next, we'll create the PersonLedger config. This config is used to define the QLDB Ledger for the sample application.

output "PersonLedger" {
  value       = aws_qldb_ledger.ledger.id
  description = "QLDB Ledger for the sample application"
}

Create the PersonApi Output

Next, we'll create the PersonApi config. This config is used to define the API Gateway endpoint URL for the Prod stage for Person functions.

output "PersonApi" {
  value       = "${aws_api_gateway_stage.prod.invoke_url}/person"
  description = "API Gateway endpoint URL for Prod stage for Person functions"
}

Deploy the Solution

Let's deploy this thing! If you haven't done so, start the Skillmix lab session and get the account credentials. Configure your Terraform environment to use those credentials.

Then, open a terminal or command prompt, navigate to the folder with your Terraform file, and execute these commands:

# initiatlize the project 
$ terraform init 

# show the plan 
$ terraform plan 

# apply the changes 
$ terraform apply

Wait for the changes to be applied before proceeding.

Create Table and Indexes

The terraform apply step will create the QLDB ledger, but not the associated table and indexes. This could be done using another AWS Lambda function as a custom resource. For now, once the application is deployed, go into the QLDB console (or use the QLDB shell), and create a new table called Person:

CREATE TABLE Person

Then create two indexes to improve performance

CREATE INDEX ON Person (personId)
CREATE INDEX ON Person (email)

Create Person record

Create a new Person record using the curl command or a tool such as Postman. This requires an HTTP POST to the endpoint which ends /Prod/person passing in the body in JSON format:

{
    "firstName":"Matt", 
    "lastName":"Lewis", 
    "email":"matt@example.com",
    "address":"1 Test Address"
}

The curl command for this is shown below:

curl --location --request POST <your API endpoint> \
--header 'Content-Type: application/json' \
--data-raw '{
    "firstName":"Matt", 
    "lastName":"Lewis", 
    "email":"matt@example.com",
    "address":"1 Test Address"
}'

The response includes a personId which is the unique ID for the document created in QLDB. Make a note of this identifier.

Update Person record

Update the address attribute for the Person record just created. This requires an HTTP POST to the endpoint which ends /Prod/person/<personId> passing in the new address in the body in JSON format:

{
    "address":"2 Test Address"
}

The curl command for this is shown below:

curl --location --request POST <your API endpoint> \
--header 'Content-Type: application/json' \
--data-raw '{
    "address":"1 Test Address"
}'

View Current State

Retrieve the current status of the Person record by making an HTTP GET call to the endpoint which ends /Prod/person/<personId>. The curl command for this is shown below:

curl --location --request GET <your API endpoint> \
--header 'Content-Type: application/json'

View History

Retrieve the full history of all changes made to the Person record by making an HTTP GET call to the endpoint which ends /Prod/person/history/<personId>. The curl command for this is shown below:

curl --location --request GET <your API endpoint> \
--header 'Content-Type: application/json'

This will return all document revisions. Each document consists of four parts. The blockAddress tells you the location of the block in the ledger's journal. The hash is the SHA-256 generated hash covering the data and metadata sections. The data section contains the user data. The metadata section contains the system-generated metadata

[
    {
        "blockAddress": {
            "strandId": "5kelavToIcrB1Tv53EEZXg",
            "sequenceNo": 19
        },
        "hash": "0j0rPn5Bp3jGnv+EPmSDHteQivmZE2Jx4l8LJ+IPiVk=",
        "data": {
            "firstName": "Matt",
            "lastName": "Lewis",
            "email": "matt@example.com",
            "address": "1 Test Address",
            "personId": "Dzd7ggQD4qEJLp2ht4luw4"
        },
        "metadata": {
            "id": "Dzd7ggQD4qEJLp2ht4luw4",
            "version": 0,
            "txTime": "2022-01-01T23:06:50.051Z",
            "txId": "30Iytfs7xx6A81lnOHse6S"
        }
    },
    ...
]

Delete Person record

Delete the Person record by making an HTTP DELETE call to the endpoint which ends /Prod/person/<personId>. The curl command for this is shown below:

curl --location --request DELETE <your API endpoint> \
--header 'Content-Type: application/json'

This will delete the record from the current state view, but you will still be able to view the full revision history.

Source: https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-qldb-terraform