Using CDKTF To Create an AWS Lambda Function
Writing Infrastructure as Code (IaC). This post shows an example of creating a simple HTML form and serving it up in a Lambda function that can be accessed from a URL.
Join the DZone community and get the full member experience.
Join For FreeHaving not done much infrastructure before, writing Terraform seemed a pretty daunting task. Learning HCL and its nuances in a declarative manner and configuring it all for different environments is a bit of a learning curve. Creating the same in code using an imperative style seems a better path for a developer.
Setting Up
This is a simple example of using Terraforms cloud development kit (CDKTF) to create a Lambda function in AWS in Typescript.
To get started, follow their installation setup here.
Create a new project:
mkdir cdktf-lambda
cd cdktf-lambda
cdktf init --template="typescript" --providers="aws@~>4.0"
Follow the cmd prompts, and lastly:
npm i @cdktf/provider-archive@5.0.1
At the time, the dependencies were:
"dependencies": {
"@cdktf/provider-archive": "^5.0.1",
"@cdktf/provider-aws": "13.0.0",
"cdktf": "^0.15.5",
"constructs": "^10.1.310"
},
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^20.1.0",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
The directory structure will have been created like so:
The init
command provides some boilerplate code to get up and running with.
The main.ts
is the central file from which the code will run, and this is known as a TerraformStack. In this stack is where all the IaC will be placed.
Let's Go
CDKTF has the concept of providers, which are Terraform wrappers for third-party APIs such as AWS. We need to add one for AWS and one to handle the lambda archive bindings:
class MyStack extends TerraformStack {
private prefix = 'dale-test-';
private region = '<your-aws-region>'
private accountId = '<your-aws-account>';
constructor(scope: Construct, id: string) {
super(scope, id);
new ArchiveProvider(this, "archiveProvider");
new AwsProvider(this, this.prefix + "aws", {
region: this.region,
allowedAccountIds: [this.accountId],
defaultTags: [
{
tags: {
name: this.prefix +'lambda-stack',
version: "1.0",
}
}
]
});
}
}
There should be enough here to do a sanity check run:
cdktf diff
At this stage, it error'd and the tsconfig file requires the following to be added:"ignoreDeprecations": "5.0"
A successful run should show:
cdktf-lambda No changes. Your infrastructure matches the configuration.
Roles
Next, add an IAM role and a policy for the Lambda:
const role = new IamRole(this, this.prefix + "iam_for_lambda", {
assumeRolePolicy: new DataAwsIamPolicyDocument(this, this.prefix + "assume_role", {
statement: [
{
actions: [
"sts:AssumeRole"
],
effect: "Allow",
principals: [
{
identifiers: ["lambda.amazonaws.com"],
type: "Service",
},
],
}
],
}).json,
name: this.prefix + "iam_for_lambda",
});
new IamRolePolicy(this, this.prefix + "iamPolicy", {
name: this.prefix + `iamPolicy-state`,
role: role.id,
policy: new DataAwsIamPolicyDocument(this, this.prefix + "iamPolicyDoc", {
version: "2012-10-17",
statement: [
{
effect: "Allow",
actions: ["logs:CreateLogGroup"],
resources: [`arn:aws:logs:${this.region}:${this.accountId}:*`]
},
{
effect: "Allow",
actions: [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
resources: [
`arn:aws:logs:${this.region}:${this.accountId}:log-group:/aws/lambda/dale-test-manual:*`
]
}
]
}).json
});
Lambda
We'll create a simple form and place index.html
and index.js
into the dist
folder:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta data-fr-http-equiv="x-ua-compatible" content="ie=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<title>Hello from AWS Lambda!</title>
<style type="text/css">
@font-face {
font-family: "sans-serif";
}
body {
margin: 0;
font-family: "Amazon Ember", Helvetica, Arial, sans-serif;
}
h1 {
background-color: #232f3e;
color: white;
font-size: 3rem;
font-weight: 300;
margin: 0;
padding: 1rem;
text-align: center;
}
input[type="text"] {
font-family: "Amazon Ember", Helvetica, Arial, sans-serif;
flex-grow: 1;
border: 1px solid #aab7b8;
border-radius: 2px;
color: #16191f;
}
input[type="text"]:focus {
border: 1px solid #00a1c9;
box-shadow: 0 0 0 1px #00a1c9;
outline: 2px dotted transparent;
}
form {
display: flex;
flex-direction: row;
gap: 1rem;
padding-right: 3rem;
}
input[type="submit"] {
background-color: white;
border: 1px solid #545b64;
border-radius: 2px;
color: #545b64;
cursor: pointer;
font-weight: 700;
padding: .4rem 2rem;
}
input[type="submit"]:hover {
background-color: #f2f3f3;
border: 1px solid #16191f;
color: #16191f;
}
</style>
</head>
<body>
<h1>CDKTF Lambda Demo</h1>
<form action="/" method="GET">
<input name="name" placeholder="name" label="name">
<input name="location" placeholder="location" label="location">
<input type="submit" value="submit">
</form>
{formResults}
{debug}
</body>
</html>
const fs = require('fs');
let html = fs.readFileSync('index.html', {encoding: 'utf8'});
/**
* Returns an HTML page containing an interactive Web-based
* tutorial. Visit the function URL to see it and learn how
* to build with lambda.
*/
exports.handler = async (event) => {
let modifiedHTML = dynamicForm(html, event.queryStringParameters);
modifiedHTML = debug(modifiedHTML, event);
const response = {
statusCode: 200,
headers: {
'Content-Type': 'text/html',
},
body: modifiedHTML,
};
return response;
};
function debug(modifiedHTML, event) {
return modifiedHTML.replace('{debug}',
JSON.stringify(event));
}
function dynamicForm(html, queryStringParameters) {
let formres = '';
if (queryStringParameters) {
Object.values(queryStringParameters).forEach(val => {
formres = formres + val + ' ';
});
}
return html.replace('{formResults}',
'<h4>Form Submission: ' + formres + '</h4>');
}
Now, set up the lambda:
The files are archived from the dist
folder, packaged up, and set in the LambdaFunction
.
A LambdaFunctionUrl
is set up so it can be publicly accessed. It is also debugged out to see more details.
const archiveFile = new DataArchiveFile(this, this.prefix +"lambda", {
outputPath: "lambda_function_payload.zip",
sourceDir: path.resolve(__dirname, "dist"),
type: "zip",
});
const lambda = new LambdaFunction(this, this.prefix +"test_lambda", {
environment: {
variables: {
foo: "bar",
},
},
filename: "lambda_function_payload.zip",
functionName: "dale_test_auto",
handler: "index.handler",
role: role.arn,
runtime: "nodejs16.x",
sourceCodeHash: archiveFile.outputBase64Sha256,
});
const url = new LambdaFunctionUrl(this, this.prefix +'lambda-url', {
functionName: lambda.functionName,
authorizationType: 'NONE'
});
const debugOutput = new TerraformOutput(this, "lambda-function", {
value: url,
});
console.log(debugOutput);
Thats it!
Deploying
Now when running cdktf diff
we should see it will add four items:
Plan: 4 to add, 0 to change, 0 to destroy.
- # aws_iam_role.dale-test-iam_for_lambda (dale-test-iam_for_lambda) will be created
- # aws_iam_role_policy.dale-test-iamPolicy (dale-test-iamPolicy) will be created
- # aws_lambda_function.dale-test-test_lambda (dale-test-test_lambda) will be created
- # aws_lambda_function.dale-test-test_lambda (dale-test-test_lambda) will be created
Now, deploy it.
cdktf deploy
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
lambda lambda-function = {
"authorization_type" = "NONE"
"cors" = tolist([])
"function_arn" = "arn:aws:lambda:eu-west-2:<id>:function:dale_test_auto"
"function_name" = "dale_test_auto"
"function_url" = "https://<random-url>.lambda-url.eu-west-2.on.aws/"
"id" = "dale_test_auto"
"invoke_mode" = "BUFFERED"
"qualifier" = ""
"timeouts" = null /* object */
"url_id" = "<random>"
}
The function_url
now is the URL to view the lambda as shown below:
To teardown the Lambda, simply run:
cdktf destroy
Destroy complete! Resources: 4 destroyed.
The full code can be found here.
Conclusion
The code is relatively simple to create infrastructure using CDKTF. It enables a logical structuring of reusable code. Once the infrastructure grows, managing a maintaining the codebase will require less effort.
Coming from a developer standpoint, it makes sense to create the using IaC. While being a Java developer, TS was selected due to the fact the TF also writes the core library in TS although it does support other languages too but transpired ultimately back to TS.
Not covered here, but the code can be unit tested to ensure everything is wired up correctly, although this doesn't necessarily prevent error downstream when planning and applying.
Also not covered here are advanced techniques required when passing resources between stacks and references from newly created resources at run time. All possible using CDKTF, however. That may be the topic for the next post.
I hope you enjoyed the post, and thanks for reading.
Opinions expressed by DZone contributors are their own.
Comments