To start, we’ll create a HTTPS accessible lambda function that simply echoes back the contents of incoming API Gateway Lambda event. The source for this is the SpartaHTML.
For reference, the helloWorld
function is below.
import (
awsLambdaEvents "github.com/aws/aws-lambda-go/events"
spartaAPIGateway "github.com/mweagle/Sparta/v3/aws/apigateway"
)
func helloWorld(ctx context.Context,
gatewayEvent spartaAWSEvents.APIGatewayRequest) (*spartaAPIGateway.Response, error) {
logger, loggerOk := ctx.Value(sparta.ContextKeyLogger).(*zerolog.Logger)
if loggerOk {
logger.Info("Hello world structured log message")
}
// Return a message, together with the incoming input...
return spartaAPIGateway.NewResponse(http.StatusOK, &helloWorldResponse{
Message: fmt.Sprintf("Hello world 🌏"),
Request: gatewayEvent,
}), nil
}
The first requirement is to create a new API instance via sparta.NewAPIGateway().
stage := sparta.NewStage("prod")
apiGateway := sparta.NewAPIGateway("MySpartaAPI", stage)
In the example above, we’re also including a Stage value.
A non-nil
Stage value will cause the registered API to be deployed. If the Stage value is nil
, a REST API will be created,
but it will not be deployed
(and therefore not publicly accessible).
The next step is to associate a URL path with the sparta.LambdaAWSInfo
struct that represents the go function:
func spartaHTMLLambdaFunctions(api *sparta.API) []*sparta.LambdaAWSInfo {
var lambdaFunctions []*sparta.LambdaAWSInfo
lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(helloWorld),
helloWorld,
sparta.IAMRoleDefinition{})
if nil != api {
apiGatewayResource, _ := api.NewResource("/hello", lambdaFn)
// We only return http.StatusOK
apiMethod, apiMethodErr := apiGatewayResource.NewMethod("GET",
http.StatusOK,
http.StatusInternalServerError)
if nil != apiMethodErr {
panic("Failed to create /hello resource: " + apiMethodErr.Error())
}
// The lambda resource only supports application/json Unmarshallable
// requests.
apiMethod.SupportedRequestContentTypes = []string{"application/json"}
}
return append(lambdaFunctions, lambdaFn)
}
Our helloWorld
only supports GET
. We’ll see how a single lambda function can support multiple HTTP methods shortly.
The final step is to to provide the API instance to Sparta.Main()
// Register the function with the API Gateway
apiStage := sparta.NewStage("v1")
apiGateway := sparta.NewAPIGateway("SpartaHTML", apiStage)
Once the service is successfully provisioned, the Outputs
key will include the API Gateway Deployed URL (sample):
INFO[0096] ────────────────────────────────────────────────
INFO[0096] Stack Outputs
INFO[0096] ────────────────────────────────────────────────
INFO[0096] S3SiteURL Description="S3 Website URL" Value="http://spartahtml-mweagle-s3site89c05c24a06599753eb3ae4e-1w6rehqu6x04c.s3-website-us-west-2.amazonaws.com"
INFO[0096] APIGatewayURL Description="API Gateway URL" Value="https://w2tefhnt4b.execute-api.us-west-2.amazonaws.com/v1"
INFO[0096] ────────────────────────────────────────────────
Combining the API Gateway URL OutputValue
with our resource path (/hello/world/test), we get the absolute URL to our lambda function: https://w2tefhnt4b.execute-api.us-west-2.amazonaws.com/v1/hello
Let’s query the lambda function and see what the event
data is at execution time. The
snippet below is pretty printed by piping the response through jq.
$ curl -vs https://3e7ux226ga.execute-api.us-west-2.amazonaws.com/v1/hello | jq .
* Trying 52.84.237.220...
* TCP_NODELAY set
* Connected to 3e7ux226ga.execute-api.us-west-2.amazonaws.com (52.84.237.220) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.execute-api.us-west-2.amazonaws.com
* Server certificate: Amazon
* Server certificate: Amazon Root CA 1
* Server certificate: Starfield Services Root Certificate Authority - G2
> GET /v1/hello HTTP/1.1
> Host: 3e7ux226ga.execute-api.us-west-2.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 1137
< Connection: keep-alive
< Date: Mon, 29 Jan 2018 14:15:28 GMT
< x-amzn-RequestId: db7f5734-04fe-11e8-b264-c70ecab3a032
< Access-Control-Allow-Origin: http://spartahtml-mweagle-s3site89c05c24a06599753eb3ae4e-419zo4dp8n2d.s3-website-us-west-2.amazonaws.com
< Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key
< Access-Control-Allow-Methods: *
< X-Amzn-Trace-Id: sampled=0;root=1-5a6f2c80-efb0f84554384252abca6d15
< X-Cache: Miss from cloudfront
< Via: 1.1 570a1979c411cb4529fa1e711db52490.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: -UsCegiR1K3vJUFyAo9IMrWGdH8rKW6UBrtJLjxZqke19r0cxMl1NA==
<
{ [1137 bytes data]
* Connection #0 to host 3e7ux226ga.execute-api.us-west-2.amazonaws.com left intact
{
"Message": "Hello world 🌏",
"Request": {
"method": "GET",
"body": {},
"headers": {
"Accept": "*/*",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Host": "3e7ux226ga.execute-api.us-west-2.amazonaws.com",
"User-Agent": "curl/7.54.0",
"Via": "1.1 570a1979c411cb4529fa1e711db52490.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "vAFNTV5uAMeTG9JN6IORnA7LYJhZyB3jHV7vh-7lXn2uZQUR6eHQUw==",
"X-Amzn-Trace-Id": "Root=1-5a6f2c80-2b48a9c86a30b0162d8ab1f1",
"X-Forwarded-For": "73.118.138.121, 205.251.214.60",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"queryParams": {},
"pathParams": {},
"context": {
"appId": "",
"method": "GET",
"requestId": "db7f5734-04fe-11e8-b264-c70ecab3a032",
"resourceId": "401s9n",
"resourcePath": "/hello",
"stage": "v1",
"identity": {
"accountId": "",
"apiKey": "",
"caller": "",
"cognitoAuthenticationProvider": "",
"cognitoAuthenticationType": "",
"cognitoIdentityId": "",
"cognitoIdentityPoolId": "",
"sourceIp": "73.118.138.121",
"user": "",
"userAgent": "curl/7.54.0",
"userArn": ""
}
}
}
}
While this demonstrates that our lambda function is publicly accessible, it’s not immediately obvious where the *event
data is being populated.
The event data that’s actually supplied to echoS3Event
is the complete HTTP request body. This content is what the API Gateway sends to our lambda function, which is defined by the integration mapping. This event data also includes the values of any whitelisted parameters. When the API Gateway Method is defined, it optionally includes any whitelisted query params and header values that should be forwarded to the integration target. For this example, we’re not whitelisting any params, so those fields (queryParams
, pathParams
) are empty. Then for each integration target (which can be AWS Lambda, a mock, or a HTTP Proxy), it’s possible to transform the API Gateway request data and whitelisted arguments into a format that’s more amenable to the target.
Sparta uses a pass-through template that passes all valid data, with minor Body differences based on the inbound Content-Type:
#*
Provide an automatic pass through template that transforms all inputs
into the JSON payload sent to a golang function. The JSON behavior attempts to parse
the incoming HTTP body as JSON assign it to the `body` field.
See
https://forums.aws.amazon.com/thread.jspa?threadID=220274&tstart=0
http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
*#
{
"method": "$context.httpMethod",
"body" : $input.json('$'),
"headers": {
#foreach($param in $input.params().header.keySet())
"$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end
#end
},
"queryParams": {
#foreach($param in $input.params().querystring.keySet())
"$param": "$util.escapeJavaScript($input.params().querystring.get($param))" #if($foreach.hasNext),#end
#end
},
"pathParams": {
#foreach($param in $input.params().path.keySet())
"$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end
#end
},
"context" : {
"apiId" : "$util.escapeJavaScript($context.apiId)",
"method" : "$util.escapeJavaScript($context.httpMethod)",
"requestId" : "$util.escapeJavaScript($context.requestId)",
"resourceId" : "$util.escapeJavaScript($context.resourceId)",
"resourcePath" : "$util.escapeJavaScript($context.resourcePath)",
"stage" : "$util.escapeJavaScript($context.stage)",
"identity" : {
"accountId" : "$util.escapeJavaScript($context.identity.accountId)",
"apiKey" : "$util.escapeJavaScript($context.identity.apiKey)",
"caller" : "$util.escapeJavaScript($context.identity.caller)",
"cognitoAuthenticationProvider" : "$util.escapeJavaScript($context.identity.cognitoAuthenticationProvider)",
"cognitoAuthenticationType" : "$util.escapeJavaScript($context.identity.cognitoAuthenticationType)",
"cognitoIdentityId" : "$util.escapeJavaScript($context.identity.cognitoIdentityId)",
"cognitoIdentityPoolId" : "$util.escapeJavaScript($context.identity.cognitoIdentityPoolId)",
"sourceIp" : "$util.escapeJavaScript($context.identity.sourceIp)",
"user" : "$util.escapeJavaScript($context.identity.user)",
"userAgent" : "$util.escapeJavaScript($context.identity.userAgent)",
"userArn" : "$util.escapeJavaScript($context.identity.userArn)"
}
},
"authorizer": {
#foreach($param in $context.authorizer.keySet())
"$param": "$util.escapeJavaScript($context.authorizer.get($param))" #if($foreach.hasNext),#end
#end
}
}
Content-Type
)#*
Provide an automatic pass through template that transforms all inputs
into the JSON payload sent to a golang function. The default behavior passes the 'body'
key as raw string.
See
https://forums.aws.amazon.com/thread.jspa?threadID=220274&tstart=0
http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
*#
{
"method": "$context.httpMethod",
"body" : "$input.path('$')",
"headers": {
#foreach($param in $input.params().header.keySet())
"$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end
#end
},
"queryParams": {
#foreach($param in $input.params().querystring.keySet())
"$param": "$util.escapeJavaScript($input.params().querystring.get($param))" #if($foreach.hasNext),#end
#end
},
"pathParams": {
#foreach($param in $input.params().path.keySet())
"$param": "$util.escapeJavaScript($input.params().path.get($param))" #if($foreach.hasNext),#end
#end
},
"context" : {
"apiId" : "$util.escapeJavaScript($context.apiId)",
"method" : "$util.escapeJavaScript($context.httpMethod)",
"requestId" : "$util.escapeJavaScript($context.requestId)",
"resourceId" : "$util.escapeJavaScript($context.resourceId)",
"resourcePath" : "$util.escapeJavaScript($context.resourcePath)",
"stage" : "$util.escapeJavaScript($context.stage)",
"identity" : {
"accountId" : "$util.escapeJavaScript($context.identity.accountId)",
"apiKey" : "$util.escapeJavaScript($context.identity.apiKey)",
"caller" : "$util.escapeJavaScript($context.identity.caller)",
"cognitoAuthenticationProvider" : "$util.escapeJavaScript($context.identity.cognitoAuthenticationProvider)",
"cognitoAuthenticationType" : "$util.escapeJavaScript($context.identity.cognitoAuthenticationType)",
"cognitoIdentityId" : "$util.escapeJavaScript($context.identity.cognitoIdentityId)",
"cognitoIdentityPoolId" : "$util.escapeJavaScript($context.identity.cognitoIdentityPoolId)",
"sourceIp" : "$util.escapeJavaScript($context.identity.sourceIp)",
"user" : "$util.escapeJavaScript($context.identity.user)",
"userAgent" : "$util.escapeJavaScript($context.identity.userAgent)",
"userArn" : "$util.escapeJavaScript($context.identity.userArn)"
}
},
"authorizer": {
#foreach($param in $context.authorizer.keySet())
"$param": "$util.escapeJavaScript($context.authorizer.get($param))" #if($foreach.hasNext),#end
#end
}
}
The default mapping templates forwards all whitelisted data & body to the lambda function. You can see by switching on the method
field would allow a single function to handle different HTTP methods.
The next example shows how to unmarshal this data and perform request-specific actions.
Because the integration request returned a successful response, the API Gateway response body contains only our lambda’s output ($input.json('$.body')
).
To return an error that API Gateway can properly translate into an HTTP
status code, use an apigateway.NewErrorResponse type. This
custom error type includes fields that trigger integration mappings based on the
inline HTTP StatusCode. The proper error
code is extracted by lifting the code
value from the Lambda’s response body and
using a template override
If you look at the Integration Response section of the /hello/world/test resource in the Console, you’ll see a list of Regular Expression matches:
Before moving on, remember to decommission the service via:
go run application.go delete
Now that we know what data is actually being sent to our API Gateway-connected Lambda function, we’ll move on to performing a more complex operation, including returning a custom HTTP response body.