Here is some information about this architecture.
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.
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.
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"
}
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"
}
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"
}
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"
}
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}/*"]
}
}
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}/*"]
}
}
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}/*"]
}
}
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}/*"]
}
}
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}/*"]
}
}
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"
}
}
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
}
}
}
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.
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}"
}
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
}
}
}
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
}
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}"
}
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
}
}
}
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.
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}"
}
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
}
}
}
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
}
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}"
}
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
}
}
}
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
}
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}"
}
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"]
}
}
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}"
}
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}"
}
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}"
}
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}"
}
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}"
}
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"
}
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}"
}
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}"
}
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"
}
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}"
}
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}"
}
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"
}
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}"
}
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}"
}
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"
}
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}"
}
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}"
}
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"
}
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}"
}
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
}
}
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"
}
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"
}
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 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 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"
}'
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'
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 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