diff --git a/deploy/api-gateway.tf b/deploy/api-gateway.tf new file mode 100644 index 0000000000000000000000000000000000000000..9483a8c5fb68c1b4c5e2f3df265d4c8931031749 --- /dev/null +++ b/deploy/api-gateway.tf @@ -0,0 +1,123 @@ +resource "aws_api_gateway_rest_api" "main" { + name = "${local.prefix}-main" + description = "Internet facing API in order to access Lambda for DynamoDB CRUD operations" +} + +resource "aws_api_gateway_resource" "access" { + rest_api_id = aws_api_gateway_rest_api.main.id + parent_id = aws_api_gateway_rest_api.main.root_resource_id + path_part = "crud" +} + +resource "aws_api_gateway_method" "access" { + for_each = local.lambdas + rest_api_id = aws_api_gateway_rest_api.main.id + resource_id = aws_api_gateway_resource.access.id + http_method = each.value.http_method + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "lambda" { + for_each = local.lambdas + rest_api_id = aws_api_gateway_rest_api.main.id + resource_id = aws_api_gateway_resource.access.id + http_method = aws_api_gateway_method.access[each.key].http_method + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.crud[each.key].invoke_arn +} + +resource "aws_api_gateway_method" "proxy_root" { + rest_api_id = aws_api_gateway_rest_api.main.id + resource_id = aws_api_gateway_rest_api.main.root_resource_id + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "lambda_root" { + rest_api_id = aws_api_gateway_rest_api.main.id + resource_id = aws_api_gateway_method.proxy_root.resource_id + http_method = aws_api_gateway_method.proxy_root.http_method + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.index.invoke_arn +} + +resource "aws_lambda_permission" "crud" { + for_each = local.lambdas + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.crud[each.key].function_name + principal = "apigateway.amazonaws.com" + + # The "/*/*" portion grants access from any method on any resource + # within the API Gateway REST API. + source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/*/*" +} +resource "aws_lambda_permission" "index" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.index.function_name + principal = "apigateway.amazonaws.com" + + # The "/*/*" portion grants access from any method on any resource + # within the API Gateway REST API. + source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/*/*" +} + +resource "aws_api_gateway_deployment" "main" { + triggers = { + redeployment = sha1(jsonencode([ + aws_api_gateway_integration.lambda, + aws_api_gateway_integration.lambda_root + ])) + } + + rest_api_id = aws_api_gateway_rest_api.main.id + lifecycle { + create_before_destroy = true + } +} + +resource "aws_api_gateway_stage" "main" { + deployment_id = aws_api_gateway_deployment.main.id + rest_api_id = aws_api_gateway_rest_api.main.id + stage_name = terraform.workspace == "production" ? "api" : terraform.workspace +} + +resource "aws_api_gateway_account" "apigw" { + cloudwatch_role_arn = aws_iam_role.cloudwatch.arn +} + +resource "aws_iam_role" "cloudwatch" { + name = "api_gateway_cloudwatch_global" + + assume_role_policy = file("./templates/api-gateway/assume-role-policy.json") +} + +resource "aws_iam_role_policy" "cloudwatch" { + name = "default" + role = aws_iam_role.cloudwatch.id + + policy = file("./templates/api-gateway/cloud-watch-policy.json") +} + +resource "aws_api_gateway_method_settings" "general_settings" { + rest_api_id = aws_api_gateway_rest_api.main.id + stage_name = aws_api_gateway_stage.main.stage_name + method_path = "*/*" + depends_on = [aws_api_gateway_account.apigw] + + settings { + # Enable CloudWatch logging and metrics + metrics_enabled = true + data_trace_enabled = true + logging_level = "INFO" + + # Limit the rate of calls to prevent abuse and unwanted charges + throttling_rate_limit = 100 + throttling_burst_limit = 50 + } +} \ No newline at end of file diff --git a/deploy/lambda.tf b/deploy/lambda.tf index 135dd553912343c5c3e48bc53139b5bbb583b812..9406a4a8ee13f002425291a4a9fc8e939b118fc6 100644 --- a/deploy/lambda.tf +++ b/deploy/lambda.tf @@ -1,23 +1,15 @@ locals { lambda_loc = "${path.module}/lambda" - files = toset([ - for f in fileset("${local.lambda_loc}/src", "*.js") : - replace(f, ".js", "") - ]) } -resource "aws_lambda_function" "test_lambda" { - for_each = local.files - filename = data.archive_file.lambda_file[each.key].output_path - function_name = "${local.prefix}-${each.key}-crud-dynamodb" - role = aws_iam_role.iam_for_lambda.arn - handler = "${each.key}.handler" - timeout = 10 - - # The filebase64sha256() function is available in Terraform 0.11.12 and later - # For Terraform 0.11.11 and earlier, use the base64sha256() function and the file() function: - # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" - source_code_hash = filebase64sha256(data.archive_file.lambda_file[each.key].output_path) +resource "aws_lambda_function" "crud" { + for_each = local.lambdas + filename = data.archive_file.lambda_file.output_path + function_name = "${local.prefix}-${each.key}-crud-dynamodb" + role = aws_iam_role.iam_for_lambda.arn + handler = "${each.key}.handler" + timeout = 10 + source_code_hash = filebase64sha256(data.archive_file.lambda_file.output_path) runtime = "nodejs12.x" reserved_concurrent_executions = 2 layers = [ @@ -32,13 +24,25 @@ resource "aws_lambda_function" "test_lambda" { } tags = local.common_tags +} - depends_on = [ - data.archive_file.lambda_file +resource "aws_lambda_function" "index" { + filename = data.archive_file.lambda_index_file.output_path + function_name = "${local.prefix}-index" + role = aws_iam_role.iam_for_lambda.arn + handler = "index.handler" + timeout = 10 + source_code_hash = filebase64sha256(data.archive_file.lambda_index_file.output_path) + runtime = "nodejs12.x" + reserved_concurrent_executions = 2 + layers = [ + "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:14" ] + tags = local.common_tags } + resource "aws_iam_role" "iam_for_lambda" { name = "${local.prefix}-lambda" @@ -65,8 +69,13 @@ resource "aws_iam_role_policy_attachment" "lambda_insights" { } data "archive_file" "lambda_file" { - for_each = local.files type = "zip" - output_path = "${local.lambda_loc}/zip/${each.key}.zip" - source_file = "${local.lambda_loc}/src/${each.key}.js" + output_path = "${local.lambda_loc}/zip/lambda.zip" + source_dir = "${local.lambda_loc}/src" +} + +data "archive_file" "lambda_index_file" { + type = "zip" + output_path = "${local.lambda_loc}/zip/index.zip" + source_file = "${local.lambda_loc}/src/index.js" } diff --git a/deploy/lambda/src/create-one.js b/deploy/lambda/src/create-one.js index b44e75749e16d8961a605a26795da2df9fa60b2b..13a0f6ced4ca9970139d3707225f7c17e2f3b448 100644 --- a/deploy/lambda/src/create-one.js +++ b/deploy/lambda/src/create-one.js @@ -5,6 +5,7 @@ const TABLE_NAME = process.env.TABLE_NAME || ''; const PRIMARY_KEY = process.env.TABLE_KEY || ''; const RESERVED_RESPONSE = `Error: You're using AWS reserved keywords as attributes`, DYNAMODB_EXECUTION_ERROR = `Error: Execution update, caused a Dynamodb error, please take a look at your CloudWatch Logs.`; exports.handler = async (event = {}) => { + console.log(event) if (!event.body) { return { statusCode: 400, body: 'invalid request, you are missing the parameter body' }; } @@ -14,13 +15,14 @@ exports.handler = async (event = {}) => { TableName: TABLE_NAME, Item: item }; + console.log(params) try { await db.put(params).promise(); - return { statusCode: 201, body: { id:item[PRIMARY_KEY] } }; } catch (dbError) { const errorResponse = dbError.code === 'ValidationException' && dbError.message.includes('reserved keyword') ? DYNAMODB_EXECUTION_ERROR : RESERVED_RESPONSE; return { statusCode: 500, body: errorResponse }; } + return { statusCode: 201, body: JSON.stringify({ id: item[PRIMARY_KEY] }) }; }; \ No newline at end of file diff --git a/deploy/lambda/src/delete-one.js b/deploy/lambda/src/delete-one.js index dfda340a63ae49b9e559f1c54b2d3c36dfb4c199..38d898422a6f5df9bc57deb40e04633c795de88a 100644 --- a/deploy/lambda/src/delete-one.js +++ b/deploy/lambda/src/delete-one.js @@ -3,7 +3,7 @@ const db = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.TABLE_NAME || ''; const PRIMARY_KEY = process.env.TABLE_KEY || ''; exports.handler = async (event = {}) => { - const requestedItemId = event.pathParameters.id; + const requestedItemId = event.queryStringParameters.id; if (!requestedItemId) { return { statusCode: 400, body: `Error: You are missing the path parameter id` }; } @@ -13,6 +13,10 @@ exports.handler = async (event = {}) => { [PRIMARY_KEY]: requestedItemId } }; + console.log({ + event: event, + params: params + }) try { await db.delete(params).promise(); return { statusCode: 200, body: '' }; diff --git a/deploy/lambda/src/get-one.js b/deploy/lambda/src/get-one.js index f9aae2bcedd811ee85bce04ba34598316cdfd1b6..c8fa4e487284665ffad67c2436b608ca34dcba7c 100644 --- a/deploy/lambda/src/get-one.js +++ b/deploy/lambda/src/get-one.js @@ -3,7 +3,7 @@ const db = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.TABLE_NAME || ''; const PRIMARY_KEY = process.env.TABLE_KEY || ''; exports.handler = async (event = {}) => { - const requestedItemId = event.pathParameters.id; + const requestedItemId = event.queryStringParameters.id; if (!requestedItemId) { return { statusCode: 400, body: `Error: You are missing the path parameter id` }; } @@ -13,7 +13,10 @@ exports.handler = async (event = {}) => { [PRIMARY_KEY]: requestedItemId } }; - console.log({params:params}) + console.log({ + event: event, + params: params + }) try { const response = await db.get(params).promise(); return { statusCode: 200, body: JSON.stringify(response.Item) }; diff --git a/deploy/lambda/src/index.js b/deploy/lambda/src/index.js index 5ce23f71d3044cd0088d08a227c6cc8ae07ecae3..2205a118346eab7d8c61a560c36b750fecebe3b3 100644 --- a/deploy/lambda/src/index.js +++ b/deploy/lambda/src/index.js @@ -9,12 +9,9 @@ exports.handler = async (event,context) => { console.info("EVENT\n" + JSON.stringify(event, null, 2)) console.log("CONTEXT\n" + JSON.stringify(context,null,2)) console.log("VARIABLES\n" + JSON.stringify(vars,null,2)) - const res = {} const response = { statusCode: 200, - body: JSON.stringify('Hello from Lambda!'), - logStream: context.logStreamName, - message:res + body: JSON.stringify({message:'Hello from Lambda!',logStream: context.logStreamName}), }; return response; diff --git a/deploy/lambda/src/package-lock.json b/deploy/lambda/src/package-lock.json index e336d0cb91c7d0af81d06cae14e832921a241bae..c6d4b6707d0df5079fe2ea3308651567eb4ad650 100644 --- a/deploy/lambda/src/package-lock.json +++ b/deploy/lambda/src/package-lock.json @@ -1,6 +1,22 @@ { + "name": "src", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "dependencies": { + "uuid": "^8.3.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + } + }, "dependencies": { "uuid": { "version": "8.3.2", diff --git a/deploy/lambda/src/package.json b/deploy/lambda/src/package.json new file mode 100644 index 0000000000000000000000000000000000000000..1456e6dbfb6e064eaef0b06bd243f72aeda4b9ec --- /dev/null +++ b/deploy/lambda/src/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "uuid": "^8.3.2" + } +} diff --git a/deploy/lambda/src/update-one.js b/deploy/lambda/src/update-one.js index 04598e4584c9b20a8e3be0453a8b21eae911605f..943c4685a139e3d275ba263a96fc8e3e247d5d9f 100644 --- a/deploy/lambda/src/update-one.js +++ b/deploy/lambda/src/update-one.js @@ -3,11 +3,11 @@ const db = new AWS.DynamoDB.DocumentClient(); const TABLE_NAME = process.env.TABLE_NAME || ''; const PRIMARY_KEY = process.env.TABLE_KEY || ''; const RESERVED_RESPONSE = `Error: You're using AWS reserved keywords as attributes`, DYNAMODB_EXECUTION_ERROR = `Error: Execution update, caused a Dynamodb error, please take a look at your CloudWatch Logs.`; -export const handler = async (event = {}) => { +exports.handler = async (event = {}) => { if (!event.body) { return { statusCode: 400, body: 'invalid request, you are missing the parameter body' }; } - const editedItemId = event.pathParameters.id; + const editedItemId = event.queryStringParameters.id; if (!editedItemId) { return { statusCode: 400, body: 'invalid request, you are missing the path parameter id' }; } @@ -31,6 +31,10 @@ export const handler = async (event = {}) => { params.UpdateExpression += `, ${property} = :${property}`; params.ExpressionAttributeValues[`:${property}`] = editedItem[property]; }); + console.log({ + event: event, + params: params + }) try { await db.update(params).promise(); return { statusCode: 204, body: '' }; diff --git a/deploy/main.tf b/deploy/main.tf index a98748ba60659f1c3127ab73e4cf5539b86e7482..7f940c066e1a41640ee04fcb71494ecc3d03e646 100644 --- a/deploy/main.tf +++ b/deploy/main.tf @@ -33,6 +33,20 @@ locals { Owner = var.contact ManagedBy = "Terraform" } + lambdas = { + get-one = { + http_method = "GET" + } + create-one = { + http_method = "PUT" + } + delete-one = { + http_method = "DELETE" + } + update-one = { + http_method = "PATCH" + } + } } data "aws_region" "current" {} \ No newline at end of file diff --git a/deploy/outputs.tf b/deploy/outputs.tf index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2fae12f26feb0bc189626f902557918b2b3bd98c 100644 --- a/deploy/outputs.tf +++ b/deploy/outputs.tf @@ -0,0 +1,3 @@ +output "api_endpoint" { + value = aws_api_gateway_stage.main.invoke_url +} \ No newline at end of file diff --git a/deploy/templates/api-gateway/assume-role-policy.json b/deploy/templates/api-gateway/assume-role-policy.json new file mode 100644 index 0000000000000000000000000000000000000000..317b2bfeed4dc55f2b821c09855f8f16ee10ae4e --- /dev/null +++ b/deploy/templates/api-gateway/assume-role-policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } \ No newline at end of file diff --git a/deploy/templates/api-gateway/cloud-watch-policy.json b/deploy/templates/api-gateway/cloud-watch-policy.json new file mode 100644 index 0000000000000000000000000000000000000000..d93eb7890a3fd5e9c1a169e6bfb857086c9c3a40 --- /dev/null +++ b/deploy/templates/api-gateway/cloud-watch-policy.json @@ -0,0 +1,18 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents" + ], + "Resource": "*" + } + ] +} \ No newline at end of file