From 753e05dcbf0969ed88b88ee24409935a91f34799 Mon Sep 17 00:00:00 2001
From: Nicolas Ferre <nicolas.ferre@arhs-spikeseed.com>
Date: Thu, 19 Oct 2023 10:51:48 +0200
Subject: [PATCH] AITED-257: convert lawfulness to api

---
 dev.tfvars                                    |   7 +-
 main.tf                                       |   5 +
 modules/classifiers/dashboard.tf              |  12 +-
 modules/classifiers/outputs.tf                |   3 +
 modules/classifiers/variables.tf              |   4 +
 modules/lawfulness/outputs.tf                 |   4 +
 modules/lawfulness/processing_api.tf          | 179 ++++++++++++++++++
 modules/lawfulness/processing_common.tf       |  96 ++++++++++
 .../lawfulness/{ecs.tf => processing_task.tf} | 132 ++-----------
 modules/lawfulness/variables.tf               |  18 ++
 variables.tf                                  |   4 +
 11 files changed, 344 insertions(+), 120 deletions(-)
 create mode 100644 modules/classifiers/outputs.tf
 create mode 100644 modules/lawfulness/processing_api.tf
 create mode 100644 modules/lawfulness/processing_common.tf
 rename modules/lawfulness/{ecs.tf => processing_task.tf} (65%)

diff --git a/dev.tfvars b/dev.tfvars
index 3332853..d8c0bea 100644
--- a/dev.tfvars
+++ b/dev.tfvars
@@ -45,7 +45,7 @@ sagemaker_classifier_budgetary_value_classifier_model_url      = "s3://d-ew1-ted
 ecr_repository_application_name           = "ted-applications"
 ecr_repository_sagemaker_classifiers_name = "sagemaker-classifiers"
 
-applications_dashboard_repository_url = "dkr.ecr.eu-west-1.amazonaws.com/ted-applications:dashboard-v0.0.5"
+applications_dashboard_repository_url = "dkr.ecr.eu-west-1.amazonaws.com/ted-applications:dashboard-v0.0.7"
 dashboard_port                        = 7860
 
 # RDS DB
@@ -88,7 +88,8 @@ iam_policy_prefix = "D_EW1_TED_AI"
 
 # Lawfulness
 lawfulness_resource_prefix = "d-ew1-ted-ai-lawfulness"
-lawfulness_task_image      = "528719223857.dkr.ecr.eu-west-1.amazonaws.com/ted-applications:lawfulness-v0.1.0"
+lawfulness_task_image      = "528719223857.dkr.ecr.eu-west-1.amazonaws.com/ted-applications:lawfulness-v0.2.0"
+lawfulness_api_port        = 8080
 lawfulness_thread_count    = 2
 
 # Ingestion
@@ -97,4 +98,4 @@ notice_ingestion_resources_url = "dkr.ecr.eu-west-1.amazonaws.com/ted-applicatio
 
 # Notice data extraction
 notice_data_extraction_prefix        = "d-ew1-ted-ai-notice-data-extraction"
-notice_data_extraction_resources_url = "dkr.ecr.eu-west-1.amazonaws.com/ted-applications:notice_data_extraction-v0.0.1"
\ No newline at end of file
+notice_data_extraction_resources_url = "dkr.ecr.eu-west-1.amazonaws.com/ted-applications:notice_data_extraction-v0.0.1"
diff --git a/main.tf b/main.tf
index 516b9cf..6ad72da 100644
--- a/main.tf
+++ b/main.tf
@@ -72,6 +72,7 @@ module "classifiers" {
   sagemaker_classifier_budgetary_value_classifier_name           = var.sagemaker_classifier_budgetary_value_classifier_name
   ssm_classifier_endpoint_budgetary_value_classifier_name        = var.ssm_classifier_endpoint_budgetary_value_classifier_name
   project_account_id                                             = var.project_account_id
+  lawfulness_host                                                = module.lawfulness.processing_api_host
 }
 
 module "ingestion" {
@@ -138,18 +139,22 @@ module "lawfulness" {
   tags                           = var.tags
   vpc_id                         = var.vpc_id
   private_subnet_id_list         = var.private_subnet_id_list
+  private_subnet_id_az1_list     = var.private_subnet_id_az1_list
+  private_subnet_id_az2_list     = var.private_subnet_id_az2_list
   iam_policy_prefix              = var.iam_policy_prefix
   iam_role_prefix                = var.iam_role_prefix
   ssm_prefix                     = var.ssm_prefix
   resource_prefix                = var.lawfulness_resource_prefix
   task_image                     = var.lawfulness_task_image
   input_bucket_arn               = module.storage.tedai_storage_s3_buckets_map.input_bucket.arn
+  dashboard_security_group_id    = module.classifiers.dashboard_security_group_id
   db_name                        = var.db_name
   db_username                    = var.db_username
   db_host                        = module.db.host
   db_port                        = module.db.port
   db_password_ssm_parameter_arn  = module.db.password_ssm_parameter_arn
   db_password_ssm_parameter_name = module.db.password_ssm_parameter_name
+  api_port                       = var.lawfulness_api_port
   thread_count                   = var.lawfulness_thread_count
 }
 
diff --git a/modules/classifiers/dashboard.tf b/modules/classifiers/dashboard.tf
index 7bef1fa..3f144ca 100644
--- a/modules/classifiers/dashboard.tf
+++ b/modules/classifiers/dashboard.tf
@@ -53,8 +53,10 @@ resource "aws_iam_policy" "dashboard" {
         Resource = "*"
       },
       {
-        Action = ["sagemaker:InvokeEndpoint", "sagemaker:CreateEndpoint", "sagemaker:DeleteEndpoint",
-        "sagemaker:DescribeEndpoint"]
+        Action = [
+          "sagemaker:InvokeEndpoint", "sagemaker:CreateEndpoint", "sagemaker:DeleteEndpoint",
+          "sagemaker:DescribeEndpoint"
+        ]
         Effect   = "Allow"
         Resource = "*"
       },
@@ -115,6 +117,12 @@ resource "aws_ecs_task_definition" "dashboard" {
         command     = ["CMD-SHELL", format("curl -f http://localhost:%s/ || exit 1", var.dashboard_port)]
         startPeriod = 10
       }
+      environment = [
+        {
+          name  = "LAWFULNESS_API_URL"
+          value = "http://${var.lawfulness_host}"
+        }
+      ]
     }
   ])
 }
diff --git a/modules/classifiers/outputs.tf b/modules/classifiers/outputs.tf
new file mode 100644
index 0000000..5923332
--- /dev/null
+++ b/modules/classifiers/outputs.tf
@@ -0,0 +1,3 @@
+output "dashboard_security_group_id" {
+  value = aws_security_group.ecs_tasks.id
+}
\ No newline at end of file
diff --git a/modules/classifiers/variables.tf b/modules/classifiers/variables.tf
index 8e811e7..376973a 100644
--- a/modules/classifiers/variables.tf
+++ b/modules/classifiers/variables.tf
@@ -46,6 +46,10 @@ variable "dashboard_port" {
   type = number
 }
 
+variable "lawfulness_host" {
+  type = string
+}
+
 # Network
 variable "vpc_id" {
   type = string
diff --git a/modules/lawfulness/outputs.tf b/modules/lawfulness/outputs.tf
index 8a0a0e2..a193796 100644
--- a/modules/lawfulness/outputs.tf
+++ b/modules/lawfulness/outputs.tf
@@ -1,3 +1,7 @@
 output "new_notices_queue_arn" {
   value = aws_sqs_queue.new_notices.arn
 }
+
+output "processing_api_host" {
+  value = aws_alb.processing_api.dns_name
+}
diff --git a/modules/lawfulness/processing_api.tf b/modules/lawfulness/processing_api.tf
new file mode 100644
index 0000000..4ffe47e
--- /dev/null
+++ b/modules/lawfulness/processing_api.tf
@@ -0,0 +1,179 @@
+resource "aws_iam_policy" "processing_api" {
+  name = "${var.iam_policy_prefix}_LAWFULNESS_PROCESSING_API_POLICY"
+  policy = jsonencode({
+    Version = "2012-10-17"
+    Statement = [
+      {
+        Action   = ["ssm:GetParameter"]
+        Effect   = "Allow"
+        Resource = var.db_password_ssm_parameter_arn
+      },
+      {
+        Action   = ["logs:CreateLogStream", "logs:PutLogEvents"]
+        Effect   = "Allow"
+        Resource = "${aws_cloudwatch_log_group.cluster.arn}:*"
+      },
+      {
+        Action   = ["ecr:BatchGetImage*", "ecr:BatchCheck*", "ecr:Get*", "ecr:List*", "ecr:Describe*"]
+        Effect   = "Allow"
+        Resource = "*"
+      }
+    ]
+  })
+}
+
+resource "aws_iam_role" "processing_api" {
+  name                 = "${var.iam_role_prefix}_LAWFULNESS_PROCESSING_API_ROLE"
+  assume_role_policy   = data.aws_iam_policy_document.ecs_assume_role_policy.json
+  managed_policy_arns  = [aws_iam_policy.processing_api.arn]
+  permissions_boundary = "arn:aws:iam::528719223857:policy/Team_Admin_Boundary"
+}
+
+
+resource "aws_ecs_task_definition" "processing_api" {
+  family                   = "${var.resource_prefix}-processing-api"
+  requires_compatibilities = ["FARGATE"]
+  network_mode             = "awsvpc"
+  cpu                      = 1024
+  memory                   = 2048
+  execution_role_arn       = aws_iam_role.processing_api.arn
+  task_role_arn            = aws_iam_role.processing_api.arn
+  container_definitions = jsonencode([
+    {
+      name               = "lawfulness"
+      image              = var.task_image
+      cpu                = 1024
+      memory             = 2048
+      execution_role_arn = aws_iam_role.processing_api.arn
+      task_role_arn      = aws_iam_role.processing_api.arn
+      essential          = true
+      logConfiguration = {
+        logDriver = "awslogs"
+        options = {
+          awslogs-group         = aws_cloudwatch_log_group.cluster.name
+          awslogs-region        = var.region
+          awslogs-stream-prefix = "ecs"
+        }
+      }
+      portMappings = [
+        {
+          containerPort = var.api_port,
+          hostPort      = var.api_port
+        }
+      ]
+      healthCheck = {
+        command     = ["CMD-SHELL", "curl -f http://localhost:${var.api_port}/health || exit 1"]
+        startPeriod = 10
+      }
+      environment = concat(local.task_environment, [{ name = "MODE", value = "api" }])
+    }
+  ])
+}
+
+resource "aws_security_group" "processing_api" {
+  name        = "${var.resource_prefix}-processing-api"
+  description = "Allow internet and service port access in lawfulness processing API"
+  vpc_id      = var.vpc_id
+
+  ingress {
+    protocol        = "tcp"
+    from_port       = var.api_port
+    to_port         = var.api_port
+    security_groups = [aws_security_group.processing_api_load_balancer.id]
+  }
+
+  egress {
+    protocol    = "-1"
+    from_port   = 0
+    to_port     = 0
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+}
+
+resource "aws_ecs_service" "processing_api" {
+  name            = "${var.resource_prefix}-processing-api"
+  cluster         = aws_ecs_cluster.cluster.id
+  task_definition = aws_ecs_task_definition.processing_api.arn
+  desired_count   = 1
+  launch_type     = "FARGATE"
+
+  network_configuration {
+    security_groups  = [aws_security_group.processing_api.id]
+    subnets          = var.private_subnet_id_list
+    assign_public_ip = false
+  }
+
+  load_balancer {
+    target_group_arn = aws_alb_target_group.processing_api.id
+    container_name   = "lawfulness"
+    container_port   = var.api_port
+  }
+  depends_on = [aws_iam_role.processing_api, aws_alb.processing_api]
+}
+
+resource "aws_security_group" "processing_api_load_balancer" {
+  name        = "${var.resource_prefix}-processing-api-load-lalancer"
+  description = "Controls access to the ALB of lawfulness API"
+  vpc_id      = var.vpc_id
+
+  ingress {
+    protocol        = "tcp"
+    from_port       = 80
+    to_port         = 80
+    security_groups = [var.dashboard_security_group_id]
+  }
+
+  egress {
+    protocol    = "-1"
+    from_port   = 0
+    to_port     = 0
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+}
+
+resource "random_shuffle" "az1" {
+  input        = var.private_subnet_id_az1_list
+  result_count = 1
+}
+
+resource "random_shuffle" "az2" {
+  input        = var.private_subnet_id_az2_list
+  result_count = 1
+}
+
+resource "aws_alb" "processing_api" {
+  name            = "lawfulness-processing-api"
+  subnets         = concat(random_shuffle.az1.result, random_shuffle.az2.result)
+  security_groups = [aws_security_group.processing_api_load_balancer.id]
+  internal        = true
+}
+
+resource "aws_alb_target_group" "processing_api" {
+  name        = "lawfulness-processing-api"
+  port        = var.api_port
+  protocol    = "HTTP"
+  vpc_id      = var.vpc_id
+  target_type = "ip"
+
+  health_check {
+    healthy_threshold   = "3"
+    interval            = "30"
+    protocol            = "HTTP"
+    matcher             = "200"
+    timeout             = "3"
+    path                = "/health"
+    unhealthy_threshold = "3"
+  }
+}
+
+# Redirect all traffic from the ALB to the target group
+resource "aws_alb_listener" "processing_api" {
+  load_balancer_arn = aws_alb.processing_api.id
+  port              = 80
+  protocol          = "HTTP"
+
+  default_action {
+    target_group_arn = aws_alb_target_group.processing_api.id
+    type             = "forward"
+  }
+}
diff --git a/modules/lawfulness/processing_common.tf b/modules/lawfulness/processing_common.tf
new file mode 100644
index 0000000..bb63f85
--- /dev/null
+++ b/modules/lawfulness/processing_common.tf
@@ -0,0 +1,96 @@
+resource "aws_ecs_cluster" "cluster" {
+  name = var.resource_prefix
+
+  setting {
+    name  = "containerInsights"
+    value = "enabled"
+  }
+
+  configuration {
+    execute_command_configuration {
+      logging = "OVERRIDE"
+      log_configuration {
+        cloud_watch_log_group_name = aws_cloudwatch_log_group.cluster.name
+      }
+    }
+  }
+}
+
+resource "aws_cloudwatch_log_group" "cluster" {
+  name = "/tedai/lawfulness"
+}
+
+resource "aws_ecs_cluster_capacity_providers" "cluster" {
+  cluster_name       = aws_ecs_cluster.cluster.name
+  capacity_providers = ["FARGATE"]
+
+  default_capacity_provider_strategy {
+    base              = 1
+    weight            = 100
+    capacity_provider = "FARGATE"
+  }
+}
+
+data "aws_iam_policy_document" "ecs_assume_role_policy" {
+  statement {
+    actions = ["sts:AssumeRole"]
+
+    principals {
+      type        = "Service"
+      identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"]
+    }
+  }
+}
+
+locals {
+  task_environment = [
+    {
+      name  = "LOG_LEVEL"
+      value = "DEBUG"
+    },
+    {
+      name  = "THREAD_COUNT"
+      value = tostring(var.thread_count)
+    },
+    {
+      name  = "FLAGGED_NOTICES_DYNAMODB_TABLE_NAME"
+      value = aws_dynamodb_table.flagged_notices.name
+    },
+    {
+      name  = "MAX_WHITELISTED_OFFICIAL_NAME_SIMILARITY"
+      value = "0.8"
+    },
+    {
+      name  = "DB_HOST"
+      value = var.db_host
+    },
+    {
+      name  = "DB_PORT"
+      value = var.db_port
+    },
+    {
+      name  = "DB_NAME"
+      value = var.db_name
+    },
+    {
+      name  = "DB_USERNAME"
+      value = var.db_username
+    },
+    {
+      name  = "DB_PASSWORD_SSM_PARAMETER"
+      value = var.db_password_ssm_parameter_name
+    },
+    {
+      name  = "DB_WHITELISTED_BODIES_TABLE"
+      value = "whitelisted_contracting_bodies"
+    },
+    {
+      name  = "NEW_NOTICES_QUEUE_URL"
+      value = aws_sqs_queue.new_notices.url
+    },
+    {
+      name  = "NEW_NOTICES_BATCH_SIZE"
+      value = "10"
+    },
+  ]
+}
diff --git a/modules/lawfulness/ecs.tf b/modules/lawfulness/processing_task.tf
similarity index 65%
rename from modules/lawfulness/ecs.tf
rename to modules/lawfulness/processing_task.tf
index 5e688c5..49ab747 100644
--- a/modules/lawfulness/ecs.tf
+++ b/modules/lawfulness/processing_task.tf
@@ -1,47 +1,3 @@
-resource "aws_ecs_cluster" "cluster" {
-  name = var.resource_prefix
-
-  setting {
-    name  = "containerInsights"
-    value = "enabled"
-  }
-
-  configuration {
-    execute_command_configuration {
-      logging = "OVERRIDE"
-      log_configuration {
-        cloud_watch_log_group_name = aws_cloudwatch_log_group.cluster.name
-      }
-    }
-  }
-}
-
-resource "aws_cloudwatch_log_group" "cluster" {
-  name = "/tedai/lawfulness"
-}
-
-resource "aws_ecs_cluster_capacity_providers" "cluster" {
-  cluster_name       = aws_ecs_cluster.cluster.name
-  capacity_providers = ["FARGATE"]
-
-  default_capacity_provider_strategy {
-    base              = 1
-    weight            = 100
-    capacity_provider = "FARGATE"
-  }
-}
-
-data "aws_iam_policy_document" "ecs_assume_role_policy" {
-  statement {
-    actions = ["sts:AssumeRole"]
-
-    principals {
-      type        = "Service"
-      identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"]
-    }
-  }
-}
-
 resource "aws_iam_policy" "processing_task" {
   name = "${var.iam_policy_prefix}_LAWFULNESS_PROCESSING_TASK_POLICY"
   policy = jsonencode({
@@ -57,11 +13,6 @@ resource "aws_iam_policy" "processing_task" {
         Effect   = "Allow"
         Resource = "${var.input_bucket_arn}/*"
       },
-      {
-        Action   = ["sqs:ReceiveMessage", "sqs:DeleteMessage"]
-        Effect   = "Allow"
-        Resource = aws_sqs_queue.new_notices.arn
-      },
       {
         Action   = ["dynamodb:PutItem"]
         Effect   = "Allow"
@@ -93,6 +44,18 @@ resource "aws_iam_role" "processing_task" {
   permissions_boundary = "arn:aws:iam::528719223857:policy/Team_Admin_Boundary"
 }
 
+resource "aws_security_group" "processing_task" {
+  name        = "${var.resource_prefix}-processing-task"
+  description = "Allow internet access in lawfulness processing task"
+  vpc_id      = var.vpc_id
+
+  egress {
+    protocol    = "-1"
+    from_port   = 0
+    to_port     = 0
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+}
 
 resource "aws_ecs_task_definition" "processing_task" {
   family                   = "${var.resource_prefix}-processing-task"
@@ -119,73 +82,11 @@ resource "aws_ecs_task_definition" "processing_task" {
           awslogs-stream-prefix = "ecs"
         }
       }
-      environment = [
-        {
-          name  = "LOG_LEVEL"
-          value = "DEBUG"
-        },
-        {
-          name  = "THREAD_COUNT"
-          value = tostring(var.thread_count)
-        },
-        {
-          name  = "NEW_NOTICES_QUEUE_URL"
-          value = aws_sqs_queue.new_notices.url
-        },
-        {
-          name  = "NEW_NOTICES_BATCH_SIZE"
-          value = "10"
-        },
-        {
-          name  = "FLAGGED_NOTICES_DYNAMODB_TABLE_NAME"
-          value = aws_dynamodb_table.flagged_notices.name
-        },
-        {
-          name  = "DB_HOST"
-          value = var.db_host
-        },
-        {
-          name  = "DB_PORT"
-          value = var.db_port
-        },
-        {
-          name  = "DB_NAME"
-          value = var.db_name
-        },
-        {
-          name  = "DB_USERNAME"
-          value = var.db_username
-        },
-        {
-          name  = "DB_PASSWORD_SSM_PARAMETER"
-          value = var.db_password_ssm_parameter_name
-        },
-        {
-          name  = "WHITELISTED_BODIES_TABLE_NAME"
-          value = "whitelisted_contracting_bodies"
-        },
-        {
-          name  = "MAX_WHITELISTED_OFFICIAL_NAME_SIMILARITY"
-          value = "0.8"
-        },
-      ],
+      environment = concat(local.task_environment, [{ name = "MODE", value = "task" }])
     }
   ])
 }
 
-resource "aws_security_group" "processing" {
-  name        = "${var.resource_prefix}-processing-task"
-  description = "Allow internet access in lawfulness processing task"
-  vpc_id      = var.vpc_id
-
-  egress {
-    protocol    = "-1"
-    from_port   = 0
-    to_port     = 0
-    cidr_blocks = ["0.0.0.0/0"]
-  }
-}
-
 resource "aws_scheduler_schedule" "processing_scheduler" {
   name                = "${var.resource_prefix}-processing-scheduler"
   group_name          = "default"
@@ -204,7 +105,7 @@ resource "aws_scheduler_schedule" "processing_scheduler" {
       launch_type         = "FARGATE"
 
       network_configuration {
-        security_groups  = [aws_security_group.processing.id]
+        security_groups  = [aws_security_group.processing_task.id]
         subnets          = var.private_subnet_id_list
         assign_public_ip = false
       }
@@ -217,6 +118,7 @@ resource "aws_scheduler_schedule" "processing_scheduler" {
   }
 }
 
+
 data "aws_iam_policy_document" "scheduler_assume_role_policy" {
   statement {
     actions = ["sts:AssumeRole"]
@@ -236,12 +138,12 @@ resource "aws_iam_policy" "processing_scheduler" {
       {
         Action   = ["ecs:RunTask"]
         Effect   = "Allow"
-        Resource = aws_ecs_task_definition.processing_task.arn_without_revision
+        Resource = [aws_ecs_task_definition.processing_task.arn_without_revision]
       },
       {
         Action   = ["iam:PassRole"]
         Effect   = "Allow"
-        Resource = aws_iam_role.processing_task.arn
+        Resource = [aws_iam_role.processing_task.arn]
       }
     ]
   })
diff --git a/modules/lawfulness/variables.tf b/modules/lawfulness/variables.tf
index e0cf895..50d4c6c 100644
--- a/modules/lawfulness/variables.tf
+++ b/modules/lawfulness/variables.tf
@@ -14,6 +14,14 @@ variable "private_subnet_id_list" {
   type = list(string)
 }
 
+variable "private_subnet_id_az1_list" {
+  type = list(string)
+}
+
+variable "private_subnet_id_az2_list" {
+  type = list(string)
+}
+
 variable "iam_policy_prefix" {
   type = string
 }
@@ -38,6 +46,10 @@ variable "input_bucket_arn" {
   type = string
 }
 
+variable "dashboard_security_group_id" {
+  type = string
+}
+
 variable "db_host" {
   type = string
 }
@@ -62,6 +74,12 @@ variable "db_password_ssm_parameter_arn" {
   type = string
 }
 
+
+variable "api_port" {
+  type = number
+}
+
 variable "thread_count" {
   type = number
 }
+
diff --git a/variables.tf b/variables.tf
index 3cd2d82..ee0c4b9 100644
--- a/variables.tf
+++ b/variables.tf
@@ -248,6 +248,10 @@ variable "lawfulness_task_image" {
   type = string
 }
 
+variable "lawfulness_api_port" {
+  type = number
+}
+
 variable "lawfulness_thread_count" {
   type = number
 }
-- 
GitLab