Deploy AWS serverless API with Go and Terraform

In the tutorial, we will explore how to deploy a simple API in a serverless manner using Go, Terraform, AWS API Gateway, and AWS Lambda. We will create an API Gateway and integrate it with our Lambda function, use Terraform to define our infrastructure as code and automate the deployment process.

Deploy AWS serverless API with Go and Terraform
Deploy AWS serverless API with Go and Terraform

Overview

In the previous article "Deploy Go AWS lambda function using Terraform", we explored how to deploy a simple AWS Lambda function written in Go using Terraform. We learned how to define our infrastructure as code and automate the deployment process, making it easy to reproduce our setup and scale our application.

In this article, we will build on that foundation and extend our application by adding an API Gateway to expose our Lambda function to the world. We will continue to use Terraform to define our infrastructure as code and automate the deployment process, but this time we will also use AWS API Gateway to create a RESTful API that can be used to invoke our Lambda function.

By the end of this tutorial, you will have a fully functional API that can be used to interact with your Lambda function and a repeatable deployment process that makes it easy to update and scale your infrastructure. So let's get started and learn how to deploy a simple API using Go, Terraform, AWS API Gateway, and AWS Lambda.

All the code can be found in the tutorial GitHub repo.

If you don't want to miss similar articles and stay up to date on our latest content, subscribe to the newsletter 📩 and never miss an update!

Go lambda function

First, let's write a code for our lambda function.

package main

import (
	"context"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func HandleRequest(ctx context.Context, event *events.APIGatewayV2HTTPRequest) (*events.APIGatewayV2HTTPResponse, error) {
	fmt.Println("event", event)

	return &events.APIGatewayV2HTTPResponse{
		StatusCode: 200,
		Body:       "You are awesome :)",
		Headers: map[string]string{
			"Content-Type": "text/plain",
		},
	}, nil
}

func main() {
	lambda.Start(HandleRequest)
}
main.go

The main change that we provide to the function is a response type. API Gateway does the transformation of HTTP requests into the input event for lambda and receives the response from lambda also as a response event to convert them into HTTP response. These event types are implemented in a github.com/aws/aws-lambda-go/events package, that we use.

Final project structure

The project structure for our project will look like this:

tutorial/
|-- lambda/
|   `-- hello-world/
|      `-- main.go
|-- api_gateway.tf
|-- iam.tf
|-- lambda.tf
|-- locals.tf
`-- main.tf

As we can see, the only difference with the previous guide is api_gateway.tf file, where we will have all the API Gateway resources.

Create AWS resources

To set up API Gateway for your lambda function we need to use the code for lambda resource from the previous tutorial. We are going to deploy our hello-world function with the new resource naming for convenience:

// create the lambda function from zip file
resource "aws_lambda_function" "hello_world" {
  function_name = "hello-world"
  description   = "My first hello world function"
  role          = aws_iam_role.lambda.arn
  handler       = local.binary_name
  memory_size   = 128

  filename         = local.archive_path
  source_code_hash = data.archive_file.function_archive.output_base64sha256

  runtime = "go1.x"
}
lambda.tf

Create API Gateway

To create API Gateway we need to use resource aws_apigatewayv2_api:

// create an API gateway for HTTP API
resource "aws_apigatewayv2_api" "simple_api" {
  name          = "simple-api"
  protocol_type = "HTTP"
  description   = "Serverless API gateway for HTTP API and AWS Lambda function"

  cors_configuration {
    allow_headers = ["*"]
    allow_methods = [
      "GET",
    ]
    allow_origins = [
      "*" // NOTE: here we should provide a particular domain, but for the sake of simplicity we will use "*"
    ]
    expose_headers = []
    max_age        = 0
  }
}
api_gateway.tf

Here we provide a basic cors policy, protocol, name, and description.

Note: For tutorial purpose, we provided a "*" as allow_origin  policy value, that should be avoided on production environment. We should specify a list of particular resources, from where we expect the request.

Additionally, we need to specify the stage for the API gateway. Stages are a way to manage and deploy multiple versions of your API for AWS API Gateway. For example, you may have a prod stage for your live production environment and a dev stage for development and testing. We are going to create one stage golang for the tutorial:

// create a stage for API GW
resource "aws_apigatewayv2_stage" "simple_api" {
  api_id = aws_apigatewayv2_api.simple_api.id

  name        = "golang"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gw.arn

    format = jsonencode({
      requestId               = "$context.requestId"
      sourceIp                = "$context.identity.sourceIp"
      requestTime             = "$context.requestTime"
      protocol                = "$context.protocol"
      httpMethod              = "$context.httpMethod"
      resourcePath            = "$context.resourcePath"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      responseLength          = "$context.responseLength"
      integrationErrorMessage = "$context.integrationErrorMessage"
      }
    )
  }
  depends_on = [aws_cloudwatch_log_group.api_gw]
}
api_gateway.tf

Finally, we need logs for our gateway:

// create logs for API GW
resource "aws_cloudwatch_log_group" "api_gw" {
  name = "/aws/api_gw/${aws_apigatewayv2_api.simple_api.name}"

  retention_in_days = 7
}
api_gateway.tf

Create API Gateway lambda integration

To allow API Gateway to invoke lambda when an HTTP request is made, we need to create an API Gateway integration, that can be a Lambda function, an HTTP webpage, or an AWS service action. In our case, we are going to create lambda integration:

// create lambda function to invoke lambda when specific HTTP request is made via API GW
resource "aws_apigatewayv2_integration" "hello_world_lambda" {
  api_id = aws_apigatewayv2_api.simple_api.id

  integration_uri  = aws_lambda_function.hello_world.arn
  integration_type = "AWS_PROXY"
}
api_gateway.tf

Here we specify the type of lambda integration as AWS_PROXY. This type of integration provides seamless integration for direct lambda invocation by API Gateway providing all necessary HTTP request fields as lambda events. Other integration types, like AWS, HTTP,  MOCK, HTTP_PROXY, with detailed explanations, can be found in the official docs.

To specify when the lambda should be triggered, we need to create an API Gateway route:

// specify route that will be used to invoke lambda function
resource "aws_apigatewayv2_route" "hello_world_lambda" {
  api_id    = aws_apigatewayv2_api.simple_api.id
  route_key = "GET /api/v1/hello"
  target    = "integrations/${aws_apigatewayv2_integration.hello_world_lambda.id}"
}
api_gateway.tf

This set up our lambda to be triggered for GET requests with /api/v1/hello path.

We also need to provide permission for the gateway to invoke the lambda function:

// provide permission for API GW to invoke lambda function
resource "aws_lambda_permission" "hello_world_lambda" {
  statement_id  = "tutorial-api-gateway-hello"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.hello_world.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.simple_api.execution_arn}/*/*"
}
api_gateway.tf

The last thing we should add for simplicity is output with created API URL:

output "api_url" {
  value = aws_apigatewayv2_api.simple_api.api_endpoint
}
api_gateway.tf

It will help to get the API endpoint for the newly created API gateway, so we can make requests to it for testing.

Let's deploy

As usual, we are going to plan the resources first:

terraform init
terraform plan

We should see:

...

Plan: 12 to add, 0 to change, 0 to destroy.

Looks correct, so let's deploy it using

terraform apply

and typing yes.

After the deployment is completed we should see the result and our output api_url:

Apply complete! Resources: 12 added, 0 changed, 0 destroyed.

Outputs:

api_url = "https://bwtdt0lujk.execute-api.us-east-1.amazonaws.com"
binary_path = "./tf_generated/hello-world"

You should have your own api_url subdomain, as AWS generates it uniquely.

Validate that Go API works

To validate the deployment we should make a get request to the endpoint api_url and specify the path to our lambda function integration. API Gateway builds the path as "${api_url}/${stage}/${route}" so in our case we should make a request to https://bwtdt0lujk.execute-api.us-east-1.amazonaws.com/golang/api/v1/hello. As it is a GET request we can just go to the URL in the browser and see the response:

You are awesome :)

Destroy resources

To destroy all the resources we created we need to run:

terraform destroy

and click yes.

Conclusion

In conclusion, we have seen how to deploy a simple API using Go, Terraform, AWS API Gateway, and AWS Lambda. By using these powerful tools, we can create a reliable and scalable infrastructure that can handle a large number of requests.

Hope this tutorial has been helpful in demonstrating the power of Terraform and AWS to automate the deployment process and create a fully functional API on AWS.

If you enjoyed this article and want to stay up to date on our latest content, subscribe to the newsletter 📩 and never miss an update!

Thank you for reading and happy coding 💻

Subscribe to TheDevBook

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe