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