Home Creating a Lambda in Golang with Cloud Development kit for Terraform - CDKTF as IAC - Part 2
Post
Cancel

Creating a Lambda in Golang with Cloud Development kit for Terraform - CDKTF as IAC - Part 2

Creating a Lambda in Golang with Cloud Development kit for Terraform - CDKTF as IAC - Part 2

In our previous post in this CDKFT AWS Lambda series, we:

  • Created a way to archive our Golang Lambda Function
  • Created our S3 Bucket
  • Created a mechanism to upload our binary to S3
  • Created a Role for our Lambda to assume
  • Created a Lambda function
  • Created a way to achieve redeploy whenever our binary changes

But our final goal was to have our Lambda Function being publicly available via an API Gateway Endpoint and a DynamoDb Table to store and retrieve data from our Lambda Function.

Creating an API Gateway

In order to have our Lambda function being available behind an API GW endpoint, we will need to create the resource first.

So let’s start by inspecting an example in Terraform and just mimic it behaviour in Go CDKTF

In our case we will be using the example provided by Terraform documentation on api_gateway_integration resource.

From the example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# API Gateway
resource "aws_api_gateway_rest_api" "api" {
  name = "myapi"
}

resource "aws_api_gateway_resource" "resource" {
  path_part   = "resource"
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.api.id
}

resource "A" "method" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.resource.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "integration" {
  rest_api_id             = aws_api_gateway_rest_api.api.id
  resource_id             = aws_api_gateway_resource.resource.id
  http_method             = aws_api_gateway_method.method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.lambda.invoke_arn
}

# Lambda
resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.function_name
  principal     = "apigateway.amazonaws.com"

	  # More: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html
  source_arn = "arn:aws:execute-api:${var.myregion}:${var.accountId}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.resource.path}"
}

We can see that we will need to create:

  • aws_api_gateway_rest_api

    So that we create the api_gateway itself

  • aws_api_gateway_resource

    So that we create an api gw resource entry

  • aws_api_gateway_method

    So that we define which methods we will allow

  • aws_api_gateway_integration

So that we can tell api gateway which “backend” to use, in this case our Lambda

  • aws_lambda_permission

And the permission to allow the api gateway to execute lambda on our behalf

Ok, so let’s start by creating the rest_api resource, for that we must first import the go module that implements it.

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewayrestapi"

Followed by appending to our AWSLambda method below our previously created in last post resources.

1
2
3
apiGwRestApi := apigatewayrestapi.NewApiGatewayRestApi(stack, jsii.String("cdktf-api-gw-rest-api"), &apigatewayrestapi.ApiGatewayRestApiConfig{
		Name: jsii.String("ilhicas-cdktf-api-gw"),
	})

As usual it follows the same structure as previously, and are able to “translate” the resource into a declarative usage in go of the resource we found in Terraform.

So now, that we have create our api_gateway_rest_api resource, let’s create the api resource defined in terraform as aws_api_gateway_resource.

So, as usual, let’s import it

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewayresource"

And let’s define our aws_api_gateway_resource as code.

1
2
3
4
5
apiGwResource := apigatewayresource.NewApiGatewayResource(stack, jsii.String("cdktf-api-gw-resource"), &apigatewayresource.ApiGatewayResourceConfig{
		PathPart:  jsii.String("resource"),
		ParentId:  apiGwRestApi.RootResourceId(),
		RestApiId: apiGwRestApi.Id(),
	})

Our aws_api_gateway_method follows the same approach, first we import it

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewaymethod"

And we define it

1
2
3
4
5
6
apiGwMethod := apigatewaymethod.NewApiGatewayMethod(stack, jsii.String("cdktf-api-gw-method"), &apigatewaymethod.ApiGatewayMethodConfig{
		RestApiId:     apiGwRestApi.Id(),
		ResourceId:    apiGwResource.Id(),
		HttpMethod:    jsii.String("GET"),
		Authorization: jsii.String("NONE"),
	})

Remember always use jsii to define your strings or other values in order to maintain interoperability across languages and for translation, as we are using CDKTF in golang, but it need to translate it to Typescript in order for code generation of Terraform itself.

If you want to know more about the internals of CDKTF don’t forget to visit the docs.

Ok, so now finally we need to define aws_api_gateway_integration in order to complete the creation of our API GW, before we jump into the permission.

So let’s import its go module

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewayintegration"

And implement it

1
2
3
4
5
6
7
8
apiGwIntegration := apigatewayintegration.NewApiGatewayIntegration(stack, jsii.String("cdktf-api-gw-lambda-integration"), &apigatewayintegration.ApiGatewayIntegrationConfig{
		RestApiId:             apiGwRestApi.Id(),
		ResourceId:            apiGwResource.Id(),
		HttpMethod:            apiGwMethod.HttpMethod(),
		IntegrationHttpMethod: jsii.String("POST"),
		Type:                  jsii.String("AWS_PROXY"),
		Uri:                   lambda.InvokeArn(),
	})

Ok, so now, all that is missing from the Terraform example, is implementing the lambda permission aws_lambda_permission

And this is where it will get tricky, in order to generate:

1
source_arn = "arn:aws:execute-api:${var.myregion}:${var.accountId}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.resource.path}"

We could, statically type region and account id, and will use Token as mentioned in previous post to obtain values in the future. Given that we are already defining statically in a previous resource the region, we will use the jsii.String() method, but we want to dynamically retrieve the account id of the caller as we would, and use cdkt tokens to retrieve future values of yet-to-be-created resources.

So let’s implement our first data resourece to obtain caller identity, as if we would do it like this in Terraform.

1
data "aws_caller_identity" "current" {}

So in order to that, let’s import it first.

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/dataawscalleridentity"

As a reminder, all of the imports for resources are the name itself, if you wish to reference a Data you just prepend your import with data.

1
callerIdentity := dataawscalleridentity.NewDataAwsCallerIdentity(stack, jsii.String("current"), &dataawscalleridentity.DataAwsCallerIdentityConfig{})

Ok, so now that we have the caller identity, let’s make use of go Sprintf and cdkt.Token_as_String methods to concatenate our string for the data_source_arn in order to achieve

1
source_arn = "arn:aws:execute-api:${var.myregion}:${var.accountId}:${aws_api_gateway_rest_api.api.id}/*/${aws_api_gateway_method.method.http_method}${aws_api_gateway_resource.resource.path}"

But in go compatible code.

1
2
3
4
5
6
7
8
lambdaPermissionSourceArn := jsii.String(fmt.Sprintf("arn:aws:execute-api:%s:%s:%s/*/%s%s",
		*jsii.String("us-east-1"),
		*callerIdentity.AccountId(),
		*cdktf.Token_AsString(apiGwRestApi.Id(), &cdktf.EncodingOptions{}),
		*cdktf.Token_AsString(apiGwMethod.HttpMethod(), &cdktf.EncodingOptions{}),
		*cdktf.Token_AsString(apiGwResource.Path(), &cdktf.EncodingOptions{}),
	),
	)

And so finally we can then create our lambda_permission resource.

First, let’s import it

1
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/lambdapermission"

And now, let’s add our resource referencing our lambdaPermissionSourceArn we have just created.

1
2
3
4
5
6
7
lambdapermission.NewLambdaPermission(stack, jsii.String("cktf-api-gw-lambda-permission"), &lambdapermission.LambdaPermissionConfig{
		StatementId:  jsii.String("AllowExecutionFromAPIGateway"),
		Action:       jsii.String("lambda:InvokeFunction"),
		FunctionName: lambda.FunctionName(),
		Principal:    jsii.String("apigateway.amazonaws.com"),
		SourceArn:    lambdaPermissionSourceArn,
	})

Ok, so now let’s add an output to where we can find the endpoint for our api and cdktf deploy.

Creating the output is similar to the creation of resources or data resources.

1
2
3
4
cdktf.NewTerraformOutput(stack, jsii.String("lamda-endpoint"), &cdktf.TerraformOutputConfig{
		Value: jsii.String(fmt.Sprintf("%s.execute-api.%s.amazonaws.com%s",
			*apiGwRestApi.Id(), "us-east-1", *apiGwResource.Path())),
	})

And now let’s do a cdktf deploy and validate our endpoint.

1
cdktf deploy

Should output something like this:

1
2
3
4
5
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

                  Outputs:
cdktf-aws-lambda
lamda-endpoint = https://ptvbwry0rd.execute-api.us-east-1.amazonaws.com/resource

Ok, so if we try to go to the output url, we should be getting a message stating forbidden from AWS API Gateway with a 403 status, and the response in JSON stating Forbidden should look like this:

1
{"message":"Forbidden"}

Ok, so in order to fix the Forbidden response in AWS API Gateway we must first do a Lambda API Deploy, which means we will also need to create a Stage for our api.

So according to the docs for Terraform, we should add two different resources:

  • aws_api_gateway_deployment
  • aws_api_gateway_stage

Ok, so lets do our import for both of them

1
2
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewaydeployment"
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/apigatewaystage"

So in order to translate the triggers condition, we must first create it with reference-able tokens

1
2
3
4
5
6
7
referenceableListTrigger := []*string{cdktf.Token_AsString(apiGwIntegration.Id(), &cdktf.EncodingOptions{}),
		cdktf.Token_AsString(apiGwMethod.Id(), &cdktf.EncodingOptions{}),
		cdktf.Token_AsString(apiGwResource.Id(), &cdktf.EncodingOptions{}),
	}
	conditionTrigger := &map[string]*string{
		"redeployment": cdktf.Fn_Sha1(cdktf.Fn_Jsonencode(referenceableListTrigger)),
	}

So we pick up on the values using the Token_AsString method to create an array And we create a map for the conditions using the Sha1 function to validate whenever these changes so we redeploy the api.

So, our api deployment resource should look like this

1
2
3
4
apiGwDeployment := apigatewaydeployment.NewApiGatewayDeployment(stack, jsii.String("cdktf-api-gw-deployment"), &apigatewaydeployment.ApiGatewayDeploymentConfig{
		RestApiId: apiGwRestApi.Id(),
		Triggers:  conditionTrigger,
	})

And our api stage is fairly simple and should look like this:

1
2
3
4
5
apigatewaystage.NewApiGatewayStage(stack, jsii.String("cdktf-api-gw-stage-prd"), &apigatewaystage.ApiGatewayStageConfig{
		DeploymentId: apiGwDeployment.Id(),
		RestApiId: apiGwRestApi.Id(),
		StageName: jsii.String("prd"),
	})

We could have used the stage as stack argument (same as region etc), but for simplicity we are creating as a static value.

So, now that we have the deployment, we can now make our life easier on the output for the lambda url by using the Stage invoke_url attribute in the output.

So, it should now look like

1
2
3
cdktf.NewTerraformOutput(stack, jsii.String("lamda-endpoint"), &cdktf.TerraformOutputConfig{
		Value: jsii.String(fmt.Sprintf("%s%s", *apiGwStage.InvokeUrl(), *apiGwResource.Path())),
	})

And now, all we need to do, is redeploy our stack

1
cdktf deploy

Which for this particular example, outputs:

1
2
cdktf-aws-lambda
  lamda-endpoint = https://ptvbwry0rd.execute-api.us-east-1.amazonaws.com/prd/resource

Ok, so by visiting the page, we no longer receive a Forbidden 403 status, but rather an Internal Server Error.

This is because, our sample function, doesn’t comply with AWS API GW response model

You can find the Docs in here

But our lambda function must respond in the following format

1
2
3
4
5
6
7
8
{
  "isBase64Encoded": false,
  "statusCode": 200,
  "body": "{ \"message\": \"Some message output!\" }",
  "headers": {
    "content-type": "application/json"
  }
}

So, let’s create a struct to contain the response modelled to be equal to the format required.

We add

1
2
3
4
5
6
type APIGatewayResponse struct {
	StatusCode      int               `json:"statusCode"`
	Headers         map[string]string `json:"headers"`
	Body            string            `json:"body"`
	IsBase64Encoded bool              `json:"isBase64Encoded,omitempty"`
}

And we change our handler to return the above structure

1
2
3
4
5
6
7
8
func HandleRequest(ctx context.Context, name MyEvent) (APIGatewayResponse, error) {
	return APIGatewayResponse{
		StatusCode:      200,
		Headers:         make(map[string]string),
		Body:            "Hello",
		IsBase64Encoded: false,
	}, nil
}

So, we now must build and deploy the function again.

1
2
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o lambda/main lambda/main.go && \
cdktf deploy

Getting a deploy output similar to

1
2
3
4
5
6
7
8
9
...
Apply complete! Resources: 0 added, 2 changed, 0 destroyed.

                  Outputs:

                  lamda-endpoint = "https://ptvbwry0rd.execute-api.us-east-1.amazonaws.com/prd/resource"

  cdktf-aws-lambda
  lamda-endpoint = https://ptvbwry0rd.execute-api.us-east-1.amazonaws.com/prd/resource

Visiting the lambda-endpoint output url we should now receive with a 200 Status ok

1
Hello

And that’s it, we now have a publicly available, functional lambda that we can update later on.

Let’s try to return a json response instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// This won't work
type APIGatewayResponse struct {
	StatusCode      int               `json:"statusCode"`
	Headers         map[string]string `json:"headers"`
	Body            map[string]string  `json:"body"`
	IsBase64Encoded bool              `json:"isBase64Encoded,omitempty"`
}

func HandleRequest(ctx context.Context, name MyEvent) (APIGatewayResponse, error) {
	return APIGatewayResponse{
		StatusCode:      200,
		Headers:         make(map[string]string),
		Body:            map[string]string{"key": "value"},
		IsBase64Encoded: false,
	}, nil
}

Well, the above won’t work and we will be presented with an Internal Server Error if we deploy our lambda without complying to the body attribute in Json being a string, also we haven’t defined our headers to “”content-type”: “application/json” either.

So, in order to return a Json response in Body in our API Gateway Proxy, we need to use what is commonly known as “JSON.stringify”.

In order to achieve that in go, we will need to use “encoding/json” library so that we Marshal our response to a string.

So, this is what it should look like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type APIGatewayResponse struct {
	StatusCode      int               `json:"statusCode"`
	Headers         map[string]string `json:"headers"`
	Body            string            `json:"body"`
	IsBase64Encoded bool              `json:"isBase64Encoded,omitempty"`
}

type Response struct {
	Message string `json:"message"`
	Level   string `json:"level"`
}

func HandleRequest(ctx context.Context, name MyEvent) (APIGatewayResponse, error) {
	body, _ := json.Marshal(Response{
		Message: "Hello",
		Level:   "INFO",
	})
	return APIGatewayResponse{
		StatusCode: 200,
		Headers: map[string]string{
			"content-type": "application/json",
		},
		Body:            string(body),
		IsBase64Encoded: false,
	}, nil
}

So now we are Stringifying our structure with json annotations in order to be able to return as a a valid response to API Gateway Proxy.

All we need to do is build and deploy our new version.

1
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o lambda/main lambda/main.go && cdktf deploy

So, now when we visit our endpoint, we should have the response as json.

1
{"message":"Hello","level":"INFO"}

Next steps, will be creating a Dynamodb table and allowing our lambda to interact with it.

Creating a DynamoDB

Ok, so now that we have our API GW creation, resource creation, permissions to interact with the lambda function, the deployment of the api, its stage, and we have already mechanisms in place to update the lambda and be able to test our endpoint, let’s now create a DynamoDB Table so that we can use it within our Lambda function.

So, first let’s create a DynamoDB Table within go with CDKTF

1
2
3
4
5
6
7
8
9
10
dynamoTable := dynamodbtable.NewDynamodbTable(stack, jsii.String("cdktf-dynamodb-table"), &dynamodbtable.DynamodbTableConfig{
		Name:        jsii.String("cdktf-ilhicas-dynamodb"),
		BillingMode: jsii.String("PAY_PER_REQUEST"),
		HashKey:     jsii.String("RequestId"),
		RangeKey:    jsii.String("Value"),
		Attribute: &[]dynamodbtable.DynamodbTableAttribute{
			{Name: jsii.String("RequestId"), Type: jsii.String("S")},
			{Name: jsii.String("Value"), Type: jsii.String("S")},
		},
	})

So, in here things get a bit different from the blocks definition in Terraform for Attribute (the schema itself), but this is how you would define a list of blocks, by passing the interface provided in this case from a list of DynamodbTableAttribute struct.

This is valid for all other cases where you find a list of block definitions in terraform.

1
&[]resource.block{ { attr: x }, { attr: y }, ..}

Ok, so now we have created the table, but we still need to grant permissions to access the table and attach those permissions to the role that our dynamodb table is currently using.

We could go and change the assume_role_policy that we defined earlier for our lambda function, but it might not be ideal.

So first let’s create a specific policy that would allow any role with this policy to do Read and Write operations on our newly created Table.

For this let’s import

1
2
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/iamrolepolicyattachment"
"github.com/cdktf/cdktf-provider-aws-go/aws/v10/iampolicy"

And now let’s use fmt.sprintfto generate a policy while substituting our values from the output of previous resources.

So our policy statement would look like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
policyRw := fmt.Sprintf(`{
		"Version": "2012-10-17",
		"Statement": [
			{
				"Sid": "DynamoDBTableAccess",
				"Effect": "Allow",
				"Action": [
					"dynamodb:BatchGetItem",
					"dynamodb:BatchWriteItem",
					"dynamodb:ConditionCheckItem",
					"dynamodb:PutItem",
					"dynamodb:DescribeTable",
					"dynamodb:DeleteItem",
					"dynamodb:GetItem",
					"dynamodb:Scan",
					"dynamodb:Query",
					"dynamodb:UpdateItem"
				],
				"Resource": "%s"
			}
		]
	}`, *dynamoTable.Arn())

Hint, while writing this post, I’ve lost several time with issues with a leading \n\t in the output of json.

The errors were generic with the following message:

1
2
[ERROR] default - 
 Error: "policy" contains an invalid JSON policy
1
"policy": "\n\t{\n\t\t\"Version\": \"2012-10-17\",\n\t\t\"Statement\": [\n\t\t\t{\n\t\t\t\t\"Sid\": \"DynamoDBTableAccess\",\n\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\"Action\": [\n\t\t\t\t\t\"dynamodb:BatchGetItem\",\n\t\t\t\t\t\"dynamodb:BatchWriteItem\",\n\t\t\t\t\t\"dynamodb:ConditionCheckItem\",\n\t\t\t\t\t\"dynamodb:PutItem\",\n\t\t\t\t\t\"dynamodb:DescribeTable\",\n\t\t\t\t\t\"dynamodb:DeleteItem\",\n\t\t\t\t\t\"dynamodb:GetItem\",\n\t\t\t\t\t\"dynamodb:Scan\",\n\t\t\t\t\t\"dynamodb:Query\",\n\t\t\t\t\t\"dynamodb:UpdateItem\"\n\t\t\t\t],\n\t\t\t\t\"Resource\": \"${aws_dynamodb_table.cdktf-dynamodb-table.arn}\"\n\t\t\t}\n\t\t]\n\t}"

This was because originally my sprintf had a newline

The error output was too generic and unhelpful for the error I was facing, but the problem was based on the following code.

1
2
3
fmt.Sprintf(`
{ ...
}`, arg )

This breaks the json output to an invalid JSON policy due to the leading newline, it should happen with empty spaces or other leading characters.

And our final cdk.tf.json inline policy should have the field without leading newlines.

1
"policy": "{\n\t\t\"Version\": \"2012-10-17\",\n\t\t\"Statement\": [\n\t\t\t{\n\t\t\t\t\"Sid\": \"DynamoDBTableAccess\",\n\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\"Action\": [\n\t\t\t\t\t\"dynamodb:BatchGetItem\",\n\t\t\t\t\t\"dynamodb:BatchWriteItem\",\n\t\t\t\t\t\"dynamodb:ConditionCheckItem\",\n\t\t\t\t\t\"dynamodb:PutItem\",\n\t\t\t\t\t\"dynamodb:DescribeTable\",\n\t\t\t\t\t\"dynamodb:DeleteItem\",\n\t\t\t\t\t\"dynamodb:GetItem\",\n\t\t\t\t\t\"dynamodb:Scan\",\n\t\t\t\t\t\"dynamodb:Query\",\n\t\t\t\t\t\"dynamodb:UpdateItem\"\n\t\t\t\t],\n\t\t\t\t\"Resource\": \"${aws_dynamodb_table.cdktf-dynamodb-table.arn}\"\n\t\t\t}\n\t\t]\n\t}"

So, now that we fixed that issue our policy resource, should look like this

1
2
3
4
5
rwDynamodbPolicy := iampolicy.NewIamPolicy(stack, jsii.String("cdktf-dynamodb-policy-rw"), &iampolicy.IamPolicyConfig{
		Name:        jsii.String("cdktf-dynamodb-policy-rw"),
		Description: jsii.String("Read And Write Permissions for CDKTF DynamodbTable"),
		Policy:      jsii.String(policyRw),
	})

So, now all we have to do, is attach this newly created policy, to the Role that the Lambda function is already using.

1
2
3
4
iamrolepolicyattachment.NewIamRolePolicyAttachment(stack, jsii.String("cdktf-lambda-dynamodb-policy-attachment"), &iamrolepolicyattachment.IamRolePolicyAttachmentConfig{
		Role:      lambdaRole.Name(),
		PolicyArn: rwDynamodbPolicy.Arn(),
	})

Ok, by now we have all the AWS resources we need to deploy our Lambda with permissions to write and read from the dynamodb table we just created.

All we need to do is actually implement the code to Read and Write from the Dynamodb Table from our Lambda function endpoint.

First, we need to add the Table name to the environment variables that will be consumed by this Lambda Function, so let’s change our Lambda to include the environment variable for the table name.

We will add the following to our Resource Attributes for

1
2
3
4
5
6
lambda := lambdafunction.NewLambdaFunction(... keep the same as previous)
...
Environment: &lambdafunction.LambdaFunctionEnvironment{
			Variables: &map[string]*string{*jsii.String("TABLE_NAME"): jsii.String("cdktf-ilhicas-dynamodb")},
		},

And on our lambda/main.go/HandleRequest we add this same Env variable to be retrieved from OS

1
tableName:= aws.String(os.Getenv("TABLE_NAME")

We will also need to map our table attributes to a struct, so let’s create a struct named item with our keys

1
2
3
4
5
type Item struct {
	RequestId string `json:"RequestId"`
	Value     string `json:"Value"`
	Error     string `json:"Error"`
}

This is not how we should design our Item output, as we should have a dedicated structure for our Messages, but for simplicity we will return always the same Item in our body response.

So, the contents of our Put and Get will be the same for this example sake, as we won’t be adding new methods for Post/Get operations and will keep the same method as before as the objective of this post is to show CDKTF abilities and have a minimal Lambda function with Put and Get Operations within our created Dynamodb.

So let’s retrieve the context of the invocation to create our item output.

In order to create our connection and make use of the role we created, the environment variables, etc, we need to import a few things.

Our whole imports should look like this

1
2
3
4
5
6
7
8
9
10
11
12
import (
	"context"
	"encoding/json"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-lambda-go/lambdacontext"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

Now let’s add our lambdacontext to retrieve the RequestId and the Value (which in our case will simply be the arn we are invoking)

1
2
3
4
5
6
7
lc, _ := lambdacontext.FromContext(ctx)
	tableName := aws.String(os.Getenv("TABLE_NAME"))
	session := session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
	out := Item{}
	db := dynamodb.New(session)

Ok, so now we have a session that will use the implicit lambda role, we are saying we want to connect to the database defined by the environment variables and no other credentials or configuration should be required.

So, now let’s implement the logic to write our Item object up to the Dynamodb table we created.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
item, err := dynamodbattribute.MarshalMap(&Item{
		RequestId: lc.AwsRequestID,
		Value:     lc.InvokedFunctionArn,
	})
	if err != nil {
		out.Error = "Invalid Request"
		return handleResponse(400, out, "ERROR")
	}
	input := &dynamodb.PutItemInput{
		Item:      item,
		TableName: tableName,
	}

	_, err = db.PutItem(input)

	if err != nil {
		out.Error = "Unable to write to the Database"
		return handleResponse(500, out, "ERROR")
	}

We retrieve the lambdacontext to fulfill our item and we create a PutItem, by marshalling the Item struct and adding the table name.

After that, all we need to do is cal the db.PutItem with the dynamodb.PutItemInput object.

And if we have any errors, we add them to the out generic Item object where we are also storing error messages, which only serves the purpose of an easier demonstration in this tutorial.

We also are calling a method named handleReponse.

This method is a generic one to avoid having so much logic within our endpoint HandleRequest method.

So, our response handling method should look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func handleResponse(status int, msg Item, level string) (APIGatewayResponse, error) {
	body, _ := json.Marshal(Response{
		Message: msg,
		Level:   level,
	})

	return APIGatewayResponse{
		StatusCode: status,
		Headers: map[string]string{
			"content-type": "application/json",
		},
		Body:            string(body),
		IsBase64Encoded: false,
	}, nil
}

And we feed this method with any error or successful invokation

So, now that we have the write to Dynamodb table created, let’s add the get, and use the same HandleRequest and continue our logic over there. The idea is just to retrieve the same item we just added.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
result, err := db.GetItem(&dynamodb.GetItemInput{
		TableName: tableName,
		Key: map[string]*dynamodb.AttributeValue{
			"RequestId": {
				S: aws.String(lc.AwsRequestID),
			},
			"Value": {
				S: aws.String(lc.InvokedFunctionArn),
			},
		},
	})
	if err != nil {
		out.Error = "Unable to read from the Database"
		return handleResponse(500, out, "ERROR")
	}

	err = dynamodbattribute.UnmarshalMap(result.Item, &out)
	if err != nil {
		out.Error = "Unable to Unmarshall item"
		return handleResponse(500, out, "ERROR")
	}
	return handleResponse(200, out, "INFO")

And that’s it for our logic changes to the lambda function to interact with the table.

Now all we need to do is build and deploy the changes made to our lambda function.

1
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o lambda/main lambda/main.go && cdktf deploy

And we should have the return output to the endpoint of our lambda function.

If we now visit the endpoint provided at (in this example) is:

1
OUR-LAMBDA-RECORD.execute-api.us-east-1.amazonaws.com/prd/resource

We should be greeted with a message similar to the one below.

1
2
3
4
5
6
7
8
{
    "message": {
        "RequestId": "7e6f4378-3184-454f-9e37-a49fe8c5fc32",
        "Value": "arn:aws:lambda:us-east-1:859786226208:function:cdktf-aws-go-lambda",
        "Error": ""
    },
    "level": "INFO"
}

And that’s it.

Don’t forget to run

cdktf destroy

To clean all created resources.

In this Tutorial Posts (part-1 and part-2) on CDKTF (Cloud Development Kit for Terraform ) we used Go to create both our infrastructure and our lambda function.

We ended up creating:

  • Lambda Function
  • API GW
  • Dynamodb

All using only golang code for it.

As usual, this post’s contents is available in Github.

This post is licensed under CC BY 4.0 by the author.