Write GraphQL API services use Go and deploy to AWS lambda & AWS APIGateway HTTP, Websocket

Tabvn
4 min readJun 11, 2022

--

Now days use Amazon web service or Google cloud is my favorites we don’t care much about server management or scale up or down based on project growing.

Tech stacks i will use in this project is:

Create HTTP server and lambda handler

package main

import (
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"
"github.com/gorilla/mux"
"github.com/thebaycity/aws-graphql/logger"
"github.com/thebaycity/aws-graphql/playground"
"github.com/thebaycity/aws-graphql/schema"
"net/http"
"os"
)

var router *mux.Router

func env(key, value string) string {
v := os.Getenv(key)
if v == "" {
return value
}
return v
}
func init() {
endpoint := env("endpoint", "http://localhost:8080/")
router = mux.NewRouter()
router.Handle("/", playground.Handler("Playground", endpoint+"graphql", os.Getenv("subscription_endpoint")))
router.Handle("/graphql", schema.Handler())
}
func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
defer func() {
_ = logger.Instance.Sync()
}()
return httpadapter.New(router).ProxyWithContext(ctx, req)
}
func main() {
if os.Getenv("exec_env") == "lambda" {
lambda.Start(handler)
} else {
http.ListenAndServe(":8080", router)
}
}

Create deployment configuration use Terraform

provider "aws" {
region = "us-west-1"
}
variable "project" {
description = "Project"
default = "aws-graphql"
}
variable "environment" {
description = "Environment"
default = "Production"
}

variable "region" {
description = "Region in which the bastion host will be launched"
default = "us-west-1"
}
data "aws_availability_zones" "available" {}
data "aws_caller_identity" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
region = "us-west-1"
}

#lambda HTTP
resource "aws_lambda_function" "aws_lambda_graphql" {
function_name = "aws-graphql"
filename = "http.zip"
role = aws_iam_role.lambda_execute.arn
runtime = "go1.x"
source_code_hash = filebase64sha256("http.zip")
handler = "http"
timeout = 10
environment {
variables = {
exec_env = "lambda"
endpoint = aws_apigatewayv2_stage.graphql.invoke_url
subscription_endpoint = aws_apigatewayv2_stage.subscription.invoke_url
region = local.region
}
}
}

resource "aws_cloudwatch_log_group" "lambda-graphql" {
name = "/aws/lambda/${aws_lambda_function.aws_lambda_graphql.function_name}"
retention_in_days = 30
}

resource "aws_cloudwatch_log_group" "lambda-subscription" {
name = "/aws/lambda/${aws_lambda_function.aws_lambda_subscription.function_name}"
retention_in_days = 30
}

resource "aws_lambda_permission" "graphql_api_gateway" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.aws_lambda_graphql.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.graphql.execution_arn}/*/*"
}

resource "aws_lambda_permission" "subscription_api_gateway" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.aws_lambda_subscription.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.subscription.execution_arn}/*/*"
}


resource "aws_lambda_function" "aws_lambda_subscription" {
function_name = "aws-graphql-subscription"
filename = "subscription.zip"
role = aws_iam_role.lambda_execute.arn
runtime = "go1.x"
source_code_hash = filebase64sha256("subscription.zip")
handler = "subscription"
timeout = 10
environment {
variables = {
region = local.region
}
}
}

resource "aws_iam_role_policy_attachment" "lambda_policy" {
role = aws_iam_role.lambda_execute.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_iam_role_policy" "api" {
name = "api_lambda_policy"
role = aws_iam_role.lambda_execute.id
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : ["execute-api:*"],
"Resource" : "arn:aws:execute-api:*:*:*",
},
]
})
}

resource "aws_iam_role_policy" "lambda-dynamodb" {
name = "dynamodb_lambda_policy"
role = aws_iam_role.lambda_execute.id
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Action" : ["dynamodb:*"],
"Resource" : aws_dynamodb_table.dynamodb.arn
}
]
})
}

resource "aws_iam_role" "lambda_execute" {
name = "lambda_execute"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}

#http
resource "aws_apigatewayv2_api" "graphql" {
name = "aws_gql_api_gateway"
protocol_type = "HTTP"
cors_configuration {
allow_origins = [
"http://localhost:3000",
"https://studio.apollographql.com"
]
allow_credentials = true
allow_headers = ["Authorization", "Content-Type", "Origin", "X-Xsrf-Token"]
allow_methods = ["GET", "POST", "DELETE", "OPTIONS"]
}
}

resource "aws_apigatewayv2_route" "root" {
api_id = aws_apigatewayv2_api.graphql.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.http.id}"
}

resource "aws_apigatewayv2_stage" "graphql" {
api_id = aws_apigatewayv2_api.graphql.id
name = "$default"
auto_deploy = true
}

resource "aws_apigatewayv2_integration" "http" {
api_id = aws_apigatewayv2_api.graphql.id
integration_uri = aws_lambda_function.aws_lambda_graphql.invoke_arn
integration_type = "AWS_PROXY"
}
resource "aws_apigatewayv2_integration" "subscription" {
api_id = aws_apigatewayv2_api.subscription.id
integration_uri = aws_lambda_function.aws_lambda_subscription.invoke_arn
integration_type = "AWS_PROXY"
}

# ps
resource "aws_apigatewayv2_api" "subscription" {
name = "aws_gql_api_gateway_subscription"
protocol_type = "WEBSOCKET"
route_selection_expression = "$request.body.*"
}

resource "aws_apigatewayv2_stage" "subscription" {
api_id = aws_apigatewayv2_api.subscription.id
name = "prod"
auto_deploy = true
}

resource "aws_apigatewayv2_route" "default_websocket_route" {
api_id = aws_apigatewayv2_api.subscription.id
route_key = "$default"
target = "integrations/${aws_apigatewayv2_integration.subscription.id}"
}
resource "aws_apigatewayv2_route" "connected_websocket_route" {
api_id = aws_apigatewayv2_api.subscription.id
route_key = "$connect"
target = "integrations/${aws_apigatewayv2_integration.subscription.id}"
}

resource "aws_apigatewayv2_route" "disconnected_websocket_route" {
api_id = aws_apigatewayv2_api.subscription.id
route_key = "$disconnect"
target = "integrations/${aws_apigatewayv2_integration.subscription.id}"
}

#dynamodb
resource "aws_dynamodb_table" "dynamodb" {
hash_key = "pk"
range_key = "sk"
name = "aws-graphql"
billing_mode = "PAY_PER_REQUEST"
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
attribute {
name = "pk"
type = "S"
}
attribute {
name = "sk"
type = "S"
}
tags = {
Name = "aws-graphql"
}
}


output "base_url" {
description = "Base URL for API Gateway stage."
value = aws_apigatewayv2_stage.graphql.invoke_url
}

output "websocket_base_url" {
description = "Base Websocket URL for API Gateway stage."
value = aws_apigatewayv2_stage.subscription.invoke_url
}

Checkout full example at: https://github.com/thebaycity/aws-graphql

Init Terraform

terraform init

Build code and deploy to AWS

GOOS=linux GOARCH=amd64 go build -o ./http lambda/http/main.go
GOOS=linux GOARCH=amd64 go build -o ./subscription lambda/subscription/main.go
zip http.zip http
rm http
zip subscription.zip subscription
rm subscription
terraform apply -auto-approve
rm http.zip
rm subscription.zip

We are using AWS APIGateway with Socket to implement subscription todo this we also need custom a graphql-ws module to implement websocket client special AWS websocket server. AWS websocket api server: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html

Look forward to see your contribute at this repository: https://github.com/thebaycity/aws-graphql

Todos:

  • Fully GraphQL subscription implemented for AWS websocket API
  • Create custom graphql-ws client and fully support with ApolloClient.

--

--

Tabvn
Tabvn

Written by Tabvn

i’m from a nice city in Vietnam called Danang, where live and did my studies. I love programming, coffee and spending my free time teaching myself new skills.

No responses yet