From b00735c2fc7b626a5a136bbe1b1067152f8b9121 Mon Sep 17 00:00:00 2001 From: edebrye <edebrye@ippon.fr> Date: Thu, 11 Mar 2021 14:10:45 +0100 Subject: [PATCH] Added api gateway --- deploy/api-gateway.tf | 123 ++++++++++++++++++ deploy/lambda.tf | 51 +++++--- deploy/lambda/src/create-one.js | 4 +- deploy/lambda/src/delete-one.js | 6 +- deploy/lambda/src/get-one.js | 7 +- deploy/lambda/src/index.js | 5 +- deploy/lambda/src/package-lock.json | 18 ++- deploy/lambda/src/package.json | 5 + deploy/lambda/src/update-one.js | 8 +- deploy/main.tf | 14 ++ deploy/outputs.tf | 3 + .../api-gateway/assume-role-policy.json | 13 ++ .../api-gateway/cloud-watch-policy.json | 18 +++ 13 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 deploy/api-gateway.tf create mode 100644 deploy/lambda/src/package.json create mode 100644 deploy/templates/api-gateway/assume-role-policy.json create mode 100644 deploy/templates/api-gateway/cloud-watch-policy.json diff --git a/deploy/api-gateway.tf b/deploy/api-gateway.tf new file mode 100644 index 0000000..9483a8c --- /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 135dd55..9406a4a 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 b44e757..13a0f6c 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 dfda340..38d8984 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 f9aae2b..c8fa4e4 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 5ce23f7..2205a11 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 e336d0c..c6d4b67 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 0000000..1456e6d --- /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 04598e4..943c468 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 a98748b..7f940c0 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 e69de29..2fae12f 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 0000000..317b2bf --- /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 0000000..d93eb78 --- /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 -- GitLab