<

Setting up a serverless api using Api Gateway, Aws Lambda, DotNetCore and terraform.

What I’m aiming to do, is create a gateway api, that will call multiple lambdas. It will look similar to a standard web api, except it will be all serverless.

We need to start by creating the api gateway, which will house all the endpoints for the service.

resource "aws_api_gateway_rest_api" "site_api" {
  name        = "site-api"
  description = "Website Api"
}

Then we need to create the base path mapping. In my scenario, I want to have an api with the following structure:

site_api/users
site_api/authentication

So I don’t need any functionality at the base path, but need to provide the mapping in order to deploy to AWS.

resource "aws_api_gateway_base_path_mapping" "site_api" {
  api_id      = "${aws_api_gateway_rest_api.site_api.id}"  
  stage_name  = "${aws_api_gateway_deployment.site_api.stage_name}"
}

The stage name refers to a deployment, so I will need to create the deployment.

resource "aws_api_gateway_deployment" "site_api" {
  depends_on = [
    "aws_api_gateway_method.users_post",
    "aws_api_gateway_integration.users_post",
    "aws_api_gateway_method.authentication_post",
    "aws_api_gateway_integration.authentication_post",
  ]

  rest_api_id       = "${aws_api_gateway_rest_api.site_api.id}"
  stage_name        = "application"
  stage_description = "1.0"
  description       = "1.0"

  lifecycle {
    create_before_destroy = true
  }
}

I know that I need to specify the dependencies in order to make sure they exist before this stage, so I specify them in the depends on section.

I now need to specify the actual resource, for the value “users”

resource "aws_api_gateway_resource" "users" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.site_api.root_resource_id}"
  path_part   = "users"
}

I then need to specify the actual http method that will be called on the site_api.

resource "aws_api_gateway_method" "users_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id   = "${aws_api_gateway_resource.users.id}"
  http_method   = "POST"
  authorization = "NONE"
}

I then need to add an integration, to call the lambda from the resource.

resource "aws_api_gateway_integration" "users_post" {
  rest_api_id             = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id             = "${aws_api_gateway_resource.users.id}"
  http_method             = "${aws_api_gateway_method.users_post.http_method}"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/${aws_lambda_function.users.arn}/invocations"
  integration_http_method = "POST"
}

I then need to make sure I add a method response, so the gateway knows is allowed to return.

resource "aws_api_gateway_method_response" "users_post_201" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.users.id}"
  http_method = "${aws_api_gateway_method.users_post.http_method}"
  status_code = "201"
}

I also want to specify for a bad request

resource "aws_api_gateway_method_response" "users_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.users.id}"
  http_method = "${aws_api_gateway_method.users_post.http_method}"
  status_code = "400"
}

I then need to do the same, for the authentication resource

resource "aws_api_gateway_resource" "authentication" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.site_api.root_resource_id}"
  path_part   = "authentication"
}

resource "aws_api_gateway_method" "authentication_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id   = "${aws_api_gateway_resource.authentication.id}"
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "authentication_post" {
  rest_api_id             = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id             = "${aws_api_gateway_resource.authentication.id}"
  http_method             = "${aws_api_gateway_method.authentication_post.http_method}"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/${aws_lambda_function.authentication.arn}/invocations"
  integration_http_method = "POST"
}

resource "aws_api_gateway_method_response" "authentication_post_201" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.authentication.id}"
  http_method = "${aws_api_gateway_method.authentication_post.http_method}"
  status_code = "201"
}

resource "aws_api_gateway_method_response" "authentication_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.authentication.id}"
  http_method = "${aws_api_gateway_method.authentication_post.http_method}"
  status_code = "400"
}

Now the endpoint integrations are setup, we can start looking at the lambdas

First we need to create a lookup to our deployed function in S3

data "aws_s3_bucket_object" "s3_build_artifact_bucket" {
  bucket = "CraigGoddenPayneBuildArtifacts"
  key    = "site-api/1.0/site-api.zip"
}

And make sure that we have a role, that can be used

resource "aws_iam_role" "lambda_role" {
  name = "site-api-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

We can then create a lambda, and point to the Function handler.

resource "aws_lambda_function" "users" {
  function_name     = "site-api-users"
  role              = "${aws_iam_role.lambda_role.arn}"
  description       = "Users"
  handler           = "SiteApi::SiteApi.Function::Users"
  runtime           = "dotnetcore2.0"
  timeout           = 30
  s3_bucket         = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.bucket}"
  s3_key            = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.key}"
  s3_object_version = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.version_id}"

  environment {
    variables = {
      Environment = "${terraform.workspace}"      
    }
  }

  vpc_config = {
    subnet_ids = [
      "${data.aws_subnet_ids.private.ids[0]}",
      "${data.aws_subnet_ids.private.ids[1]}",
    ]

    security_group_ids = ["${aws_security_group.site_api.id}"]
  }
 
  tags {
    Owner       = "Craig Godden-Payne"
    Environment = "${terraform.workspace}"
  }
}

If you want to setup security group rules, you need to add the lambda into a subnet with internet access.

data "aws_vpc" "vpc" {
  filter {
    name   = "tag:Name"
    values = ["vpc.craigs-vpc"]
  }
}

data "aws_subnet" "private" {
  count = "${length(data.aws_subnet_ids.private.ids)}"
  id    = "${data.aws_subnet_ids.private.ids[count.index]}"
}

data "aws_subnet_ids" "private" {
  vpc_id = "${data.aws_vpc.vpc.id}"

  tags {
    Name = "private.*"
  }
}

resource "aws_security_group" "site_api" {
  name        = "site-api"
  description = "site api"
  vpc_id      = "${data.aws_vpc.vpc.id}"

  tags {
    Name = "site-api security group"
  }
}

resource "aws_security_group_rule" "private_egress_all" {
  type              = "egress"
  to_port           = 65535
  protocol          = "tcp"
  from_port         = 1024
  security_group_id = "${aws_security_group.site_api.id}"
  description       = "Private access to all"
  cidr_blocks       = ["0.0.0.0/0"]
}

You need a similar setup for the authentication lambda

resource "aws_lambda_permission" "authentication" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.authentication.arn}"
  principal     = "apigateway.amazonaws.com"

  source_arn = "arn:aws:execute-api:eu-west-2:000000000000:${aws_api_gateway_rest_api.site_api.id}/*/${aws_api_gateway_method.authentication_post.http_method}${aws_api_gateway_resource.authentication.path}"
}

resource "aws_lambda_function" "authentication" {
  function_name     = "site-api-authentication"
  role              = "${aws_iam_role.lambda_role.arn}"
  description       = "Authentication"
  handler           = "SiteApi::SiteApi.Function::Authentication"
  runtime           = "dotnetcore2.0"
  timeout           = 30
  s3_bucket         = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.bucket}"
  s3_key            = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.key}"
  s3_object_version = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.version_id}"

  environment {
    variables = {
      Environment = "${terraform.workspace}"      
    }
  }

  tags {
    Owner       = "Craig Godden-Payne"
    Environment = "${terraform.workspace}"
  }
}

If you want to see the full configuration, check out the below!

resource "aws_api_gateway_rest_api" "site_api" {
  name        = "site-api"
  description = "Website Api"
}

resource "aws_api_gateway_base_path_mapping" "site_api" {
  api_id      = "${aws_api_gateway_rest_api.site_api.id}"  
  stage_name  = "${aws_api_gateway_deployment.site_api.stage_name}"
}

resource "aws_api_gateway_deployment" "site_api" {
  depends_on = [
    "aws_api_gateway_method.users_post",
    "aws_api_gateway_integration.users_post",
    "aws_api_gateway_method.authentication_post",
    "aws_api_gateway_integration.authentication_post",
  ]

  rest_api_id       = "${aws_api_gateway_rest_api.site_api.id}"
  stage_name        = "application"
  stage_description = "1.0"
  description       = "1.0"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_resource" "users" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.site_api.root_resource_id}"
  path_part   = "users"
}

resource "aws_api_gateway_method" "users_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id   = "${aws_api_gateway_resource.users.id}"
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "users_post" {
  rest_api_id             = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id             = "${aws_api_gateway_resource.users.id}"
  http_method             = "${aws_api_gateway_method.users_post.http_method}"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/${aws_lambda_function.users.arn}/invocations"
  integration_http_method = "POST"
}

resource "aws_api_gateway_method_response" "users_post_201" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.users.id}"
  http_method = "${aws_api_gateway_method.users_post.http_method}"
  status_code = "201"
}

resource "aws_api_gateway_method_response" "users_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.users.id}"
  http_method = "${aws_api_gateway_method.users_post.http_method}"
  status_code = "400"
}

resource "aws_api_gateway_resource" "authentication" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  parent_id   = "${aws_api_gateway_rest_api.site_api.root_resource_id}"
  path_part   = "authentication"
}

resource "aws_api_gateway_method" "authentication_post" {
  rest_api_id   = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id   = "${aws_api_gateway_resource.authentication.id}"
  http_method   = "POST"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "authentication_post" {
  rest_api_id             = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id             = "${aws_api_gateway_resource.authentication.id}"
  http_method             = "${aws_api_gateway_method.authentication_post.http_method}"
  type                    = "AWS_PROXY"
  uri                     = "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/${aws_lambda_function.authentication.arn}/invocations"
  integration_http_method = "POST"
}

resource "aws_api_gateway_method_response" "authentication_post_201" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.authentication.id}"
  http_method = "${aws_api_gateway_method.authentication_post.http_method}"
  status_code = "201"
}

resource "aws_api_gateway_method_response" "authentication_post_400" {
  rest_api_id = "${aws_api_gateway_rest_api.site_api.id}"
  resource_id = "${aws_api_gateway_resource.authentication.id}"
  http_method = "${aws_api_gateway_method.authentication_post.http_method}"
  status_code = "400"
}

data "aws_s3_bucket_object" "s3_build_artifact_bucket" {
  bucket = "CraigGoddenPayneBuildArtifacts"
  key    = "site-api/1.0/site-api.zip"
}
 
resource "aws_iam_role" "lambda_role" {
  name = "site-api-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_lambda_function" "users" {
  function_name     = "site-api-users"
  role              = "${aws_iam_role.lambda_role.arn}"
  description       = "Users"
  handler           = "SiteApi::SiteApi.Function::Users"
  runtime           = "dotnetcore2.0"
  timeout           = 30
  s3_bucket         = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.bucket}"
  s3_key            = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.key}"
  s3_object_version = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.version_id}"

  environment {
    variables = {
      Environment = "${terraform.workspace}"      
    }
  }

  vpc_config = {
    subnet_ids = [
      "${data.aws_subnet_ids.private.ids[0]}",
      "${data.aws_subnet_ids.private.ids[1]}",
    ]

    security_group_ids = ["${aws_security_group.site_api.id}"]
  }
 
  tags {
    Owner       = "Craig Godden-Payne"
    Environment = "${terraform.workspace}"
  }
}

data "aws_vpc" "vpc" {
  filter {
    name   = "tag:Name"
    values = ["vpc.craigs-vpc"]
  }
}

data "aws_subnet" "private" {
  count = "${length(data.aws_subnet_ids.private.ids)}"
  id    = "${data.aws_subnet_ids.private.ids[count.index]}"
}

data "aws_subnet_ids" "private" {
  vpc_id = "${data.aws_vpc.vpc.id}"

  tags {
    Name = "private.*"
  }
}


resource "aws_security_group" "site_api" {
  name        = "site-api"
  description = "site api"
  vpc_id      = "${data.aws_vpc.vpc.id}"

  tags {
    Name = "site-api security group"
  }
}

resource "aws_security_group_rule" "private_egress_all" {
  type              = "egress"
  to_port           = 65535
  protocol          = "tcp"
  from_port         = 1024
  security_group_id = "${aws_security_group.site_api.id}"
  description       = "Private access to all"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_lambda_permission" "authentication" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = "${aws_lambda_function.authentication.arn}"
  principal     = "apigateway.amazonaws.com"

  source_arn = "arn:aws:execute-api:eu-west-2:000000000000:${aws_api_gateway_rest_api.site_api.id}/*/${aws_api_gateway_method.authentication_post.http_method}${aws_api_gateway_resource.authentication.path}"
}

resource "aws_lambda_function" "authentication" {
  function_name     = "site-api-authentication"
  role              = "${aws_iam_role.lambda_role.arn}"
  description       = "Authentication"
  handler           = "SiteApi::SiteApi.Function::Authentication"
  runtime           = "dotnetcore2.0"
  timeout           = 30
  s3_bucket         = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.bucket}"
  s3_key            = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.key}"
  s3_object_version = "${data.aws_s3_bucket_object.s3_build_artifact_bucket.version_id}"

  environment {
    variables = {
      Environment = "${terraform.workspace}"      
    }
  }

  tags {
    Owner       = "Craig Godden-Payne"
    Environment = "${terraform.workspace}"
  }
}

Written on May 30, 2018.