In my previous article, How to Build and Test a PDF Generator Lambda Using LocalStack on Your Local Machine (Part 2: Using Terraform), we set up the foundational infrastructure, including S3
, ECR
, a Lambda function
, and an IAM role
using Terraform.
In this part, we’ll take it further by integrating API Gateway to invoke the Lambda function
locally using LocalStack—allowing you to simulate real-world AWS behavior entirely on your local machine. This is particularly useful when you need to trigger Lambda functions via HTTP, just as you would in a production environment.
I’ve kept this guide simple and practical, so all you need is some basic Terraform knowledge to follow along.
Let's get started!
Prerequisites
Before proceeding, ensure you have the following installed:
- Docker
- Python 3.9+
- LocalStack account
- LocalStack CLI
- LocalStack Desktop
- Terraform
1. locals.tf
In this step, we'll introduce two new local variables specifically for the API Gateway resources. These variables define the names for the API Gateway and its deployment stage:
locals {
...
api_gateway_name = "pdf-generator-api"
api_gateway_stage_name = "prod"
...
}
2. main.tf
Add the following code blocks below to the main.tf
file:
-
rest api
resource "aws_api_gateway_rest_api" "lambda_api" {
name = local.api_gateway_name
description = "API Gateway for invoking the PDF Generator Lambda function"
endpoint_configuration {
types = ["REGIONAL"]
}
}
This block above creates the API Gateway REST API
. We assign its name using the local variable we defined earlier: local.api_gateway_name
.
The endpoint_configuration is set to REGIONAL, which is suitable because; we wont be using CloudFront and the API is not private(it will be accessible within the region).
-
resource
resource "aws_api_gateway_resource" "pdf_generator_api_root" {
rest_api_id = aws_api_gateway_rest_api.lambda_api.id
parent_id = aws_api_gateway_rest_api.lambda_api.root_resource_id
path_part = "generate-pdf"
}
This block creates a new API Gateway resource
, linking it to the previously created REST API using rest_api_id
. We also define the parent_id
and set the path to /generate-pdf
, which establishes the resource's URL path under the API Gateway.
-
method
resource "aws_api_gateway_method" "pdf_generator_api_method" {
rest_api_id = aws_api_gateway_rest_api.lambda_api.id
resource_id = aws_api_gateway_resource.pdf_generator_api_root.id
http_method = "POST"
authorization = "NONE"
api_key_required = false
}
This block creates the method for the resource above. It links the method to the REST API as well as the resource, and specifies the HTTP method as POST. Since we don't need authorization or an API key in this case, we set them to "NONE" and false, respectively.
-
integration
resource "aws_api_gateway_integration" "pdf_generator_api_integration" {
rest_api_id = aws_api_gateway_rest_api.lambda_api.id
resource_id = aws_api_gateway_resource.pdf_generator_api_root.id
http_method = aws_api_gateway_method.pdf_generator_api_method.http_method
type = "AWS_PROXY"
integration_http_method = "POST"
uri = aws_lambda_function.pdf_generator.invoke_arn
depends_on = [aws_lambda_function.pdf_generator]
}
This block creates the integration for the method above, linking it to the REST API, the resource, and the HTTP method. It specifies an AWS_PROXY
integration type, which allows direct communication between API Gateway and the Lambda function. The integration HTTP
method is set to POST, and the URI points to the Lambda function’s invoke ARN. The depends_on
attribute ensures that the Lambda function is created before the integration is established.
-
deployment
resource "aws_api_gateway_deployment" "lambda_api_deployment" {
rest_api_id = aws_api_gateway_rest_api.lambda_api.id
stage_name = local.api_gateway_stage_name
lifecycle {
create_before_destroy = true
}
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.pdf_generator_api_root.id,
aws_api_gateway_method.pdf_generator_api_method.id,
aws_api_gateway_integration.pdf_generator_api_integration.id
]))
}
depends_on = [
aws_api_gateway_integration.pdf_generator_api_integration
]
}
This block creates a deployment for the API, linking it to the REST API and specifying the stage name, which was declared earlier as a local variable. The lifecycle
block ensures that the deployment is created before any resources are destroyed, minimizing downtime. The triggers
attribute uses a SHA1 hash of the resource, method, and integration IDs to trigger a redeployment when changes occur. The depends_on
attribute ensures that the deployment happens only after the API integration is fully created.
-
permission
resource "aws_lambda_permission" "apigw_lambda_permission" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.pdf_generator.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.lambda_api.execution_arn}/${local.api_gateway_stage_name}/*"
lifecycle {
replace_triggered_by = [aws_lambda_function.pdf_generator]
}
depends_on = [aws_lambda_function.pdf_generator, aws_api_gateway_deployment.lambda_api_deployment]
}
This block grants the necessary permission for the API Gateway endpoint to invoke the Lambda function. The action
is set to "lambda:InvokeFunction"
, allowing the API Gateway to trigger the Lambda. The source_arn
specifies the ARN of the API Gateway’s execution role and stage. The lifecycle
block ensures that the permission is replaced if the Lambda function changes, and the depends_on
attribute ensures the permission is created only after the Lambda function and API Gateway deployment are in place.
3. outputs.tf
Finally, create a new file named outputs.tf
in the root of the infrastructure
directory and add this content.
output "rest_api_url" {
description = "The URL of the API Gateway REST API"
value = "https://${aws_api_gateway_rest_api.lambda_api.id}.execute-api.localhost.localstack.cloud:4566/${local.api_gateway_stage_name}"
}
This block creates an output for the URL of the API Gateway REST API. Normally, the URL follows the format: https://{rest_api_id}.execute-api.{region}.amazonaws.com/{stage_name}
.
However, since we're working with LocalStack in our local environment, the URL format is slightly different. It omits the region
and amazonaws.com
parts, and instead uses localhost.localstack.cloud
as the base domain
. This allows us to interact with the API locally.
The rest_api_id
is simply the ID of the REST API, and there’s no difference in how it's used here. The stage_name
corresponds to the local variable defined earlier in the locals.tf
file
At this point, we have completed the code needed to provision the API Gateway resources and retrieve the URL. The next step is to create the resources and test our new implementation.
Run tflocal plan
to get an overview of all the AWS resources that will be created. Then, run tflocal apply
to confirm and initiate the creation of the resources.
Afterward, you should see the output of your API Gateway URL in the terminal. As explained earlier, the URL will have a format similar to this: https://{your_rest_api_id}.execute-api.localhost.localstack.cloud:4566/prod
- Invoke lambda function
To test the Lambda function, we won’t invoke it directly. Instead, we will use the URL mentioned above. You can use Postman to make a POST
request to https://{your_rest_api_id}.execute-api.localhost.localstack.cloud:4566/prod/generate-pdf
(API Gateway URL + the path to the resource we created generate-pdf
) with the following payload:
{"first_name": "John", "last_name": "Doe"}
This is the same payload we previously used with the event.json file.
Alternatively, you can use curl to make the request. Here's the curl
command:
curl -X POST https://{your_rest_api_id}.execute-api.localhost.localstack.cloud:4566/prod/generate-pdf \
-H "Content-Type: application/json" \
-d '{"first_name": "John", "last_name": "Doe"}'
You should see the success message below:
{"message": "PDF generated and uploaded successfully!"}
You can confirm that the PDF
was successfully generated and stored in the S3 bucket
by navigating to the bucket via the LocalStack Desktop
and downloading the PDF.
Congratulations! 🎉
You’ve successfully set up your API Gateway, linked it to your Lambda function, and tested it locally using Terraform and LocalStack—all without needing an AWS account. Thanks for reading!
Top comments (0)