Kirill Zonov

HOWTO: Create and integrate AWS Lambda function using Terraform

January 20, 2018 | 9 Minute Read

If you followed my 3 my previous posts - you already created your first Amazon Lambda function, made it able to write to DynamoDB and be accessible from the outside world, using API Gateway. In this post, I will guide you on how to implement the same but without touching the AWS Management Console, which is barely understandable and very volatile by the interface. Instead, we will be using Terraform, which I also covered in the past blog post. Let’s get started: If you don’t have Terraform installed or you have no clue what is it and how to use it - please read first this post, it’s pretty explanatory. If you have - please create a new file with whatever name you like, because it will be the only file. For me, it will be zonov.tf. For sure you wouldn’t like to use that name in your company. First, let’s make sure that you have your AWS provider installed for Terraform. Add a basic config to your file:

# Provider
provider "aws" {
  region = "eu-west-1"
}

And then go to the console, to the folder you saved your file and perform terraform plan. Probably it will tell you smth like: terraform plan fails

It means that you just need to perform terraform init, which will install the needed provider: terraform init installs aws provider

Then you can try to “plan” again and it will be green: terraform plan succeeds

Ok, cool, let’s start adding real resources in there. Add the following to your TF file:

# Lambda
resource "aws_iam_role" "lambda" {
  name = "kzonovGreeterRoleTF"
  assume_role_policy = "${data.aws_iam_policy_document.lambda-assume-role.json}"
}
data "aws_iam_policy_document" "lambda-assume-role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

A couple of words, about what is happening here. In the first block, we create an IAM Role, which we name “lambda” and we say that it can be assumed by service lambda.amazonaws.com. Basically, it means that we give AWS’s Lambda service the ability to work with permissions of given IAM role. Now you already can create your role on AWS. Don’t hesitate, it’s free. Again: terraform plan

terraform plan with iam role

And now you can actually create it: terraform apply

terraform apply with iam role

Yay! Ok, now it’s time to create a Lambda function. Here we will go by the easiest Hello world example, if you missed the explanation post - take a look here, in this one I’ll just paste this code into the new file lambda.js which you should put into the same folder.

exports.handler = (event, context, callback) => {
  callback(null, {
    statusCode: '200',
    body: 'Hello ' + event.queryStringParameters["name"] + '!'
  });
};

When you deploy your Lambda function to AWS, it should be packed into .zip. So we add following to our .tf file:

data "archive_file" "lambda" {
  type = "zip"
  source_file = "lambda.js"
  output_path = "lambda.zip"
}

With this, we use terraform’s tool archive_file which tadaaaam, archives provided file to the specified archive type. And the next block to your TF file.

resource "aws_lambda_function" "greeter-lambda" {
  filename = "${data.archive_file.lambda.output_path}"
  function_name = "kzonovGreeterLambdaTF"
  role = "${aws_iam_role.lambda.arn}"
  handler = "lambda.handler"
  runtime = "nodejs6.10"
  source_code_hash = "${base64sha256(file(data.archive_file.lambda.output_path))}"
}

Here we finally create our Lambda function, using the archive created by the last block, gives a described on the top role and tells that it should be interpreted using nodejs v6.10. Next, I want you to give your lambda function ability to write to Cloudwatch, it’s AWS log service. If you create a function from Management console - you’ll have it by default, but using terrafrom you should specify it manually.

resource "aws_iam_role_policy" "lambda-cloudwatch-log-group" {
  name = "kzonov-cloudwatch-log-group"
  role = "${aws_iam_role.lambda.name}"
  policy = "${data.aws_iam_policy_document.cloudwatch-log-group-lambda.json}"
}
data "aws_iam_policy_document" "cloudwatch-log-group-lambda" {
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = [
      "arn:aws:logs:::*",
    ]
  }
}

The second block is a policy document, where you specify that the role, to which you will attach it, will be able to create log stream and put logs into it. And in the first one, you attach this policy document to your lambda’s role. Lambda’s part is over, you can try to apply your config now. Don’t forget to make terraform plan first, it’s a good manner, like making git diff before adding files to the commit. I expect after plan step you’ll have an error because you have to perform terraform init in order to install archive_file. If you’re such kind of error:

No permissions aws lambda error

check that your user, you’re using for logging in, has AWSLambdaFullAccess permissions (can be checked in IAM/Users/Permissions). After applying configuration you can go to the Management Console and check that your function had been created. Next step for us is to create an API Gateway, let’s do it. First just add one block, which describes the fact that API should be created with a specific name:

resource "aws_api_gateway_rest_api" "api" {
  name = "kzonovGreeterApiTF"
}

Every API has some resources and methods for them, so let’s add such instructions:

resource "aws_api_gateway_resource" "api-resource" {
  path_part = "greetings"
  parent_id = "${aws_api_gateway_rest_api.api.root_resource_id}"
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
}
resource "aws_api_gateway_method" "method" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_resource.api-resource.id}"
  http_method = "GET"
  authorization = "NONE"
}

Here we create an API resource “greetings”, which will have one get method. So our path will be smth like https://someapi.com/greetings. Keep in mind that you should specify both rest_api_id and resource_id for API method, even if it sounds redundant for you (for me it is). The last thing we will create today is an integration of our API to the function and give permission to invoke it:

resource "aws_api_gateway_integration" "integration" {
  rest_api_id = "${aws_api_gateway_rest_api.api.id}"
  resource_id = "${aws_api_gateway_resource.api-resource.id}"
  http_method = "${aws_api_gateway_method.method.http_method}"
  integration_http_method = "POST"
  type = "AWS_PROXY"
  uri = "arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/${aws_lambda_function.greeter-lambda.arn}/invocations"
}
resource "aws_lambda_permission" "greeter-permissions" {
  statement_id = "AllowExecutionFromAPIGateway"
  action = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.greeter-lambda.arn}"
  principal = "apigateway.amazonaws.com"
}

Here we first create an integration of the specific function to the specific API method. Pay attention, the integration_http_method is POST here despite that for external HTTP it was GET. The type I chose AWS_PROXY because it’s the new and convenient type of integration AWS provides. On the second block, we just give API Gateway ability to InvokeFunction. After adding it, make plan and apply again and you should see a success message. (If you have 403 error, add AmazonAPIGatewayAdministrator permission for your user). After successful apply go to the Management console -> APIGateway -> your_api_name -> Actions -> Deploy API. Add a new stage and press Deploy. It will be deployed within a second and on the top you’ll see a URL, like https://msid0tkout.execute-api.eu-west-1.amazonaws.com/production. Don’t forget that your API resource is named “greetings” so copy the URL, add resource name and also add a query param “name” (like https://msid0tkout.execute-api.eu-west-1.amazonaws.com/production/greetings?name=Kirill). You’ve seen that we use it in our lambda, did you? So, this is your deployed API URL. Try it with your name and you’ll get: Hello Kirill! (or smth else if your name by some reason is not Kirill) Tadaam! As you see, no rocket science is here, but I hope it was helpful for you!