From b8c020cbe4fb3bf6c1e111b8777354095be126e0 Mon Sep 17 00:00:00 2001
From: Nuwan Rajika Kumarasiri <nuwan.kumarasiri@wisc.edu>
Date: Fri, 7 Feb 2020 14:01:29 -0600
Subject: [PATCH] Add an EFS volume into Secure Agent infrastructure

This volume will act as the persistence storage for Secure Agent's logs and configurations.
---
 .gitignore                                    |  1 +
 README.md                                     |  4 +-
 terraform/autoscaling-group.tf                | 42 ++++++++++++
 terraform/ecs.tf                              | 46 +++++++++----
 terraform/efs.tf                              | 14 ++++
 terraform/iam.tf                              | 30 ++++++++
 terraform/network.tf                          |  5 --
 terraform/security.tf                         | 27 +++++++-
 ...ontainer.tpl => container-definitions.tpl} | 22 +++++-
 terraform/variables.tf                        | 68 ++++++++++++++++---
 10 files changed, 222 insertions(+), 37 deletions(-)
 create mode 100644 terraform/autoscaling-group.tf
 create mode 100644 terraform/efs.tf
 create mode 100644 terraform/iam.tf
 rename terraform/templates/{container.tpl => container-definitions.tpl} (53%)

diff --git a/.gitignore b/.gitignore
index 751b4ba..2b6e4fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ sandbox
 *.tfvars
 .idea/
 *.backup
+*.tfplan
diff --git a/README.md b/README.md
index a5ada76..35d2b43 100644
--- a/README.md
+++ b/README.md
@@ -122,8 +122,8 @@ See Terraform doc on [variables](https://www.terraform.io/docs/configuration/var
 image can be used for production set up.
 
 ### Credentials in Terraform
-* It's recommended that to avoid having Informatica credentials in bash history, all the variables can be defined in a `*.tfvars` 
-file and pass to terraform using `-var-file` argument to terraform.  
+* It's recommended to define all variables values in a `*.tfvars` file and pass that to terraform using `-var-file` argument.
+ This will avoid having Informatica credentials in bash history. 
 
 ```shell script
 $ cd terraform
diff --git a/terraform/autoscaling-group.tf b/terraform/autoscaling-group.tf
new file mode 100644
index 0000000..d75278c
--- /dev/null
+++ b/terraform/autoscaling-group.tf
@@ -0,0 +1,42 @@
+resource "aws_autoscaling_group" "secure-agent-autoscaling-group" {
+  # as per our current licesning in IICS, each docker instance that
+  # runs on EC2 will treat at as a new license.
+  name = "secure-agent-autoscaling-group"
+  desired_capacity = 1
+  max_size = 1
+  min_size = 1
+
+  vpc_zone_identifier = data.aws_subnet_ids.subnets.ids
+  health_check_type = "EC2"
+  launch_configuration = aws_launch_configuration.secure-agent-launch-config.name
+}
+
+data "aws_ami" "ecs-optimized" {
+  most_recent = true
+  owners      = ["amazon"]
+
+  filter {
+    name   = "name"
+    values = ["amzn2-ami-hvm-*"]
+  }
+}
+
+resource "aws_launch_configuration" "secure-agent-launch-config" {
+  name = "secure-agnet-launch-configuration"
+  image_id      = data.aws_ami.ecs-optimized.image_id
+  enable_monitoring = false
+
+  iam_instance_profile = aws_iam_instance_profile.ecs-instance-profile.name
+  security_groups = [data.aws_security_group.sec-group.id]
+  user_data = <<EOF
+              #!/bin/bash
+              echo ECS_CLUSTER=${var.ecs_cluster_name} >> /etc/ecs/ecs.config
+              yum install -y ecs-init
+              service docker start
+              start ecs
+              EOF
+  instance_type = var.instance_type
+  lifecycle {
+    create_before_destroy = true
+  }
+}
\ No newline at end of file
diff --git a/terraform/ecs.tf b/terraform/ecs.tf
index ec0773d..5bbc9d3 100644
--- a/terraform/ecs.tf
+++ b/terraform/ecs.tf
@@ -1,16 +1,18 @@
 data "template_file" "container" {
-  template = file("./templates/container.tpl")
+  template = file("./templates/container-definitions.tpl")
   vars     = {
-    container_name       = var.container_name
-    image_name           = var.image_name
-    fargate_cpu          = var.fargate_cpu
-    fargate_memory       = var.fargate_memory
-    app_port1            = var.container_app_port[0]
-    app_port2            = var.container_app_port[1]
-    app_port3            = var.container_app_port[2]
-    network_mode         = var.container_network_mode
-    informatica_username = var.informatica_username
-    informatica_password = var.informatica_password
+    container_name          = var.container_name
+    image_name              = var.image_name
+    app_port1               = var.container_app_port[0]
+    app_port2               = var.container_app_port[1]
+    app_port3               = var.container_app_port[2]
+    informatica_username    = var.informatica_username
+    informatica_password    = var.informatica_password
+    volume1                 = var.secure_agnet_container_volumes[0]
+    volume2                 = var.secure_agnet_container_volumes[1]
+    volume3                 = var.secure_agnet_container_volumes[2]
+    volume4                 = var.secure_agnet_container_volumes[3]
+    secure_agent_efs_volume = var.secure_agent_efs_volume
   }
 }
 
@@ -23,10 +25,23 @@ resource "aws_ecs_task_definition" "task" {
   execution_role_arn       = data.aws_iam_role.ecs-task-execution.arn
   network_mode             = var.container_network_mode
   requires_compatibilities = [
-    "FARGATE"]
-  cpu                      = var.fargate_cpu
-  memory                   = var.fargate_memory
+    "EC2"]
   container_definitions    = data.template_file.container.rendered
+  volume {
+    name      = var.secure_agent_efs_volume
+    host_path = ""
+    docker_volume_configuration {
+      autoprovision = true
+      scope         = "shared"
+
+      driver_opts = {
+        "type"   = "nfs"
+        "device" = "${aws_efs_file_system.secure-agent-fs.dns_name}:/"
+        "o"      = "addr=${aws_efs_file_system.secure-agent-fs.dns_name},nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,nosuid"
+      }
+    }
+  }
+  tags                     = var.ecs_task_tags
 }
 
 resource "aws_ecs_service" "service" {
@@ -34,7 +49,8 @@ resource "aws_ecs_service" "service" {
   cluster         = aws_ecs_cluster.cluster.id
   task_definition = aws_ecs_task_definition.task.arn
   desired_count   = 1
-  launch_type     = "FARGATE"
+  # secure agent configs and logs are persisted into an EFS volume.
+  launch_type     = "EC2"
 
   network_configuration {
     security_groups  = [
diff --git a/terraform/efs.tf b/terraform/efs.tf
new file mode 100644
index 0000000..4673276
--- /dev/null
+++ b/terraform/efs.tf
@@ -0,0 +1,14 @@
+resource "aws_efs_file_system" "secure-agent-fs" {
+  tags = var.efs_tags
+}
+
+output "aws_efs_token" {
+  value = aws_efs_file_system.secure-agent-fs.creation_token
+}
+
+resource "aws_efs_mount_target" "secure-agent-fs-mount" {
+  file_system_id  = aws_efs_file_system.secure-agent-fs.id
+  subnet_id       = sort(data.aws_subnet_ids.subnets.ids)[0]
+  security_groups = [
+    data.aws_security_group.sec-group.id]
+}
\ No newline at end of file
diff --git a/terraform/iam.tf b/terraform/iam.tf
new file mode 100644
index 0000000..f3fc7c6
--- /dev/null
+++ b/terraform/iam.tf
@@ -0,0 +1,30 @@
+# define a policy document for role below
+data "aws_iam_policy_document" "ecs-agent" {
+  statement {
+    actions = [
+      "sts:AssumeRole"]
+    principals {
+      type        = "Service"
+      identifiers = [
+        "ec2.amazonaws.com"]
+    }
+  }
+}
+
+# define the role for ECS agent so that ECS container agent can make API calls
+resource "aws_iam_role" "ecs-agent" {
+  name               = var.ecs_iam_role
+  assume_role_policy = data.aws_iam_policy_document.ecs-agent.json
+}
+
+# grant role permission for ECS agent operations
+resource "aws_iam_role_policy_attachment" "ecs-agent" {
+  role       = aws_iam_role.ecs-agent.name
+  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
+}
+
+# allow instance profile to assume this role
+resource "aws_iam_instance_profile" "ecs-instance-profile" {
+  name = "secure-agent-ecs-instance-profile"
+  role = aws_iam_role.ecs-agent.name
+}
\ No newline at end of file
diff --git a/terraform/network.tf b/terraform/network.tf
index 7f9c484..9e94654 100644
--- a/terraform/network.tf
+++ b/terraform/network.tf
@@ -9,9 +9,4 @@ data "aws_subnet_ids" "subnets" {
     values = [
       var.private_subnets_filter["Name"]]
   }
-}
-
-data "aws_security_group" "sec-group" {
-  vpc_id = data.aws_vpc.vpc.id
-  tags   = var.security_group
 }
\ No newline at end of file
diff --git a/terraform/security.tf b/terraform/security.tf
index dc048a8..16f60a3 100644
--- a/terraform/security.tf
+++ b/terraform/security.tf
@@ -1,4 +1,29 @@
-data "aws_security_group" "secgroup" {
+data "aws_security_group" "sec-group" {
   vpc_id = data.aws_vpc.vpc.id
   tags   = var.security_group
+}
+
+// open port 2049 for NFSv4
+resource "aws_security_group" "secure-agent-fs-" {
+  name   = "secure-agent-efs-sg"
+  vpc_id = data.aws_vpc.vpc.id
+
+  // NFS
+  ingress {
+    security_groups = [
+      data.aws_security_group.sec-group.id]
+    from_port       = 2049
+    to_port         = 2049
+    protocol        = "tcp"
+  }
+
+  egress {
+    security_groups = [
+      data.aws_security_group.sec-group.id]
+    from_port       = 0
+    to_port         = 0
+    protocol        = "-1"
+  }
+
+  tags = var.secure_agent_sg_tags
 }
\ No newline at end of file
diff --git a/terraform/templates/container.tpl b/terraform/templates/container-definitions.tpl
similarity index 53%
rename from terraform/templates/container.tpl
rename to terraform/templates/container-definitions.tpl
index 660198c..99dbaf4 100644
--- a/terraform/templates/container.tpl
+++ b/terraform/templates/container-definitions.tpl
@@ -2,9 +2,7 @@
   {
     "name": "${container_name}",
     "image": "${image_name}",
-    "cpu": ${fargate_cpu},
-    "memory": ${fargate_memory},
-    "networkMode": "${network_mode}",
+    "memory": 4096,
     "portMappings": [
       {
         "containerPort": ${app_port1},
@@ -28,6 +26,24 @@
             "name": "INFORMATICA_PASSWORD",
             "value": "${informatica_password}"
         }
+    ],
+    "mountPoints": [
+        {
+            "containerPath": "${volume1}",
+            "sourceVolume": "${secure_agent_efs_volume}"
+        },
+        {
+            "containerPath": "${volume2}",
+            "sourceVolume": "${secure_agent_efs_volume}"
+        },
+        {
+            "containerPath": "${volume3}",
+            "sourceVolume": "${secure_agent_efs_volume}"
+        },
+        {
+            "containerPath": "${volume4}",
+            "sourceVolume": "${secure_agent_efs_volume}"
+        }
     ]
   }
 ]
\ No newline at end of file
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 007d69d..401a528 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -1,7 +1,18 @@
+variable "informatica_username" {}
+variable "informatica_password" {}
+
 variable "aws_shared_cred_file" {}
 variable "aws_profile" {
   default = "default"
 }
+
+variable "aws_account_id" {
+  type = list(string)
+  default = [
+    "265723766240"
+  ]
+}
+
 variable "aws_region" {
   # test tier
   default = "us-east-1"
@@ -57,18 +68,18 @@ variable "container_count" {
   default = 1
 }
 
-# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html
-# see Secure Agent resource requirements for these numbers.
-variable "fargate_cpu" {
-  # 1 vCPU = 1024 CPU units
-  default = "4096"
-}
-variable "fargate_memory" {
-  # in MiB
-  default = "8192"
+# see Secure Agent system requirements, before changing instance type, see the
+# supported instance types for launch configuration.
+variable "instance_type" {
+  default = "t2.large"
 }
 
 # ecs
+
+variable "ecs_iam_role" {
+  default = "iics-secure-agent-iam-role"
+}
+
 variable "ecs_cluster_name" {
   default = "iics-agent-cluster"
 }
@@ -85,9 +96,44 @@ variable "ecs_task_name" {
   default = "iics-agent-task"
 }
 
+variable "ecs_task_tags" {
+  type    = map(string)
+  default = {
+    Name = "iics-secure-agent"
+    tier = "test"
+  }
+}
+
 variable "ecs_service_name" {
   default = "iics-agent-service"
 }
 
-variable "informatica_username" {}
-variable "informatica_password" {}
\ No newline at end of file
+variable "efs_tags" {
+  type = map(string)
+  default = {
+    Name = "iics-secure-agent"
+    tier = "test"
+  }
+}
+
+variable "secure_agnet_container_volumes" {
+  type = list(string)
+  default = [
+    # see Dockerfile for these default values.
+    "/home/agent/infaagent/apps/agentcore/infaagent.log",
+    "/home/agent/infaagent/apps/agentcore/agentcore.log",
+    "/home/agent/infaagent/apps/agentcore/logs",
+    "/home/agent/infaagent/apps/agentcore/data"
+  ]
+}
+
+variable "secure_agent_efs_volume" {
+  default = "agent"
+}
+
+variable "secure_agent_sg_tags" {
+  default = {
+    Name = "secure-agent-efs-sg"
+    tier = "test"
+  }
+}
\ No newline at end of file
-- 
GitLab