diff --git a/services/terraform/modules/keyserver_node_service/main.tf b/services/terraform/modules/keyserver_node_service/main.tf
new file mode 100644
--- /dev/null
+++ b/services/terraform/modules/keyserver_node_service/main.tf
@@ -0,0 +1,199 @@
+locals {
+  environment_var_list = [
+    for name, value in var.environment_vars : {
+      name  = name
+      value = value
+    }
+  ]
+}
+
+resource "aws_cloudwatch_log_group" "service" {
+  name              = "/ecs/${var.service_name}-task-def"
+  retention_in_days = 7
+}
+
+resource "aws_ecs_task_definition" "service" {
+  network_mode             = "awsvpc"
+  family                   = "${var.service_name}-task-def"
+  requires_compatibilities = ["FARGATE"]
+  task_role_arn            = var.ecs_task_role_arn
+  execution_role_arn       = var.ecs_task_execution_role_arn
+  cpu                      = var.cpu
+  memory                   = var.memory
+
+  ephemeral_storage {
+    size_in_gib = var.ephemeral_storage
+  }
+
+  container_definitions = jsonencode([
+    {
+      name      = var.container_name
+      image     = var.image
+      essential = true
+      portMappings = [
+        {
+          name          = "${var.service_name}-port"
+          containerPort = 3000
+          hostPort      = 3000
+          protocol      = "tcp"
+        },
+      ]
+      environment = local.environment_var_list
+      logConfiguration = {
+        "logDriver" = "awslogs"
+        "options" = {
+          "awslogs-create-group"  = "true"
+          "awslogs-group"         = aws_cloudwatch_log_group.service.name
+          "awslogs-stream-prefix" = "ecs"
+          "awslogs-region"        = var.region
+        }
+      }
+      linuxParameters = {
+        initProcessEnabled = true
+      }
+    }
+  ])
+
+  runtime_platform {
+    cpu_architecture        = "ARM64"
+    operating_system_family = "LINUX"
+  }
+}
+
+resource "aws_security_group" "service" {
+  name   = "${var.service_name}-service-ecs-sg"
+  vpc_id = var.vpc_id
+
+  ingress {
+    from_port   = 3000
+    to_port     = 3000
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  ingress {
+    description      = "Allow inbound traffic from any IPv6 address"
+    from_port        = 3000
+    to_port          = 3000
+    protocol         = "tcp"
+    ipv6_cidr_blocks = ["::/0"]
+  }
+
+  egress {
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  lifecycle {
+    create_before_destroy = true
+  }
+}
+
+resource "aws_ecs_service" "service" {
+  name                               = var.service_name
+  cluster                            = var.cluster_id
+  task_definition                    = aws_ecs_task_definition.service.arn
+  launch_type                        = "FARGATE"
+  enable_execute_command             = true
+  enable_ecs_managed_tags            = true
+  force_new_deployment               = true
+  desired_count                      = var.desired_count
+  deployment_maximum_percent         = 200
+  deployment_minimum_healthy_percent = 100
+
+  network_configuration {
+    subnets          = var.vpc_subnets
+    security_groups  = [aws_security_group.service.id]
+    assign_public_ip = true
+  }
+
+  load_balancer {
+    target_group_arn = aws_lb_target_group.service.arn
+    container_name   = var.container_name
+    container_port   = 3000
+  }
+
+  deployment_circuit_breaker {
+    enable   = true
+    rollback = true
+  }
+}
+
+resource "aws_lb_target_group" "service" {
+  name     = "${var.service_name}-ecs-tg"
+  port     = 3000
+  protocol = "HTTP"
+  vpc_id   = var.vpc_id
+
+  target_type = "ip"
+
+  stickiness {
+    type            = "lb_cookie"
+    cookie_duration = 86500
+    enabled         = true
+  }
+
+  health_check {
+    enabled             = true
+    healthy_threshold   = 2
+    unhealthy_threshold = 3
+
+    protocol = "HTTP"
+    path     = "/health"
+    matcher  = "200"
+  }
+}
+
+resource "aws_lb" "service" {
+  load_balancer_type = "application"
+  name               = "${var.service_name}-lb"
+  security_groups    = [aws_security_group.lb_sg.id]
+
+  internal = false
+  subnets  = var.vpc_subnets
+}
+
+resource "aws_lb_listener" "service" {
+  load_balancer_arn = aws_lb.service.arn
+  port              = "443"
+  protocol          = "HTTPS"
+  ssl_policy        = "ELBSecurityPolicy-2016-08"
+  certificate_arn   = data.aws_acm_certificate.service.arn
+
+  default_action {
+    type             = "forward"
+    target_group_arn = aws_lb_target_group.service.arn
+  }
+
+  lifecycle {
+    ignore_changes       = [default_action[0].forward[0].stickiness[0].duration]
+    replace_triggered_by = [aws_lb_target_group.service]
+  }
+}
+
+resource "aws_security_group" "lb_sg" {
+  name        = "${var.service_name}-lb-sg"
+  description = "Security group for ${var.service_name} load balancer"
+  vpc_id      = var.vpc_id
+
+  ingress {
+    from_port   = 443
+    to_port     = 443
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  egress {
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+}
+
+data "aws_acm_certificate" "service" {
+  domain   = var.domain_name
+  statuses = ["ISSUED"]
+}
diff --git a/services/terraform/modules/keyserver_node_service/outputs.tf b/services/terraform/modules/keyserver_node_service/outputs.tf
new file mode 100644
--- /dev/null
+++ b/services/terraform/modules/keyserver_node_service/outputs.tf
@@ -0,0 +1,3 @@
+output "service_load_balancer_dns_name" {
+  value = aws_lb.service.dns_name
+}
diff --git a/services/terraform/modules/keyserver_node_service/variables.tf b/services/terraform/modules/keyserver_node_service/variables.tf
new file mode 100644
--- /dev/null
+++ b/services/terraform/modules/keyserver_node_service/variables.tf
@@ -0,0 +1,85 @@
+# AWS Deployment Configuration Options
+
+variable "region" {
+  description = "The AWS region"
+  type        = string
+}
+
+variable "vpc_id" {
+  description = "The VPC ID"
+  type        = string
+}
+
+variable "vpc_subnets" {
+  description = "List of VPC subnet IDs"
+  type        = list(string)
+}
+
+variable "cluster_id" {
+  description = "id of ecs cluster"
+  type        = string
+}
+
+variable "ecs_task_role_arn" {
+  description = "The ARN of the ECS task role"
+  type        = string
+}
+
+variable "ecs_task_execution_role_arn" {
+  description = "The ARN of the ECS task execution role"
+  type        = string
+}
+
+# Service Options
+
+variable "service_name" {
+  description = "The name of the ECS service"
+  type        = string
+}
+
+variable "domain_name" {
+  description = "The domain name for the load balancer certificate"
+  type        = string
+}
+
+variable "container_name" {
+  description = "The name of the container"
+  type        = string
+}
+
+
+variable "desired_count" {
+  description = "Desired number of running nodes"
+  type        = number
+  # default 2 for constant uptime
+  default = 2
+}
+
+variable "image" {
+  description = "The Docker image for the container"
+  type        = string
+}
+
+variable "environment_vars" {
+  description = "Map of environment variables to be initialized in container"
+  type        = map(string)
+}
+
+# Task resources
+variable "cpu" {
+  description = "CPU units allocated to each task"
+  type        = number
+  default     = 2048
+}
+
+variable "memory" {
+  description = "Memory allocated to each task in MiB"
+  type        = number
+  default     = 4096
+}
+
+variable "ephemeral_storage" {
+  description = "Ephemeral storage dedicated to task in GiB"
+  type        = number
+  default     = 40
+}
diff --git a/services/terraform/self-host/outputs.tf b/services/terraform/self-host/outputs.tf
--- a/services/terraform/self-host/outputs.tf
+++ b/services/terraform/self-host/outputs.tf
@@ -1,3 +1,7 @@
 output "keyserver_service_load_balancer_dns_name" {
   value = aws_lb.keyserver_service.dns_name
 }
+
+output "webapp_service_load_balancer_dns_name" {
+  value = var.enable_webapp_service ? module.webapp_service[0].service_load_balancer_dns_name : ""
+}
diff --git a/services/terraform/self-host/variables.tf b/services/terraform/self-host/variables.tf
--- a/services/terraform/self-host/variables.tf
+++ b/services/terraform/self-host/variables.tf
@@ -8,11 +8,6 @@
   type        = string
 }
 
-variable "webapp_domain_name" {
-  description = "Domain name for your web app"
-  type        = string
-}
-
 variable "region" {
   description = "Keyserver's AWS deployment region"
   type        = string
@@ -58,3 +53,17 @@
   type        = string
   default     = null
 }
+
+# Web app
+
+variable "enable_webapp_service" {
+  description = "Whether to run webapp on AWS"
+  type        = bool
+  default     = false
+}
+
+variable "webapp_domain_name" {
+  description = "Domain name for your web app"
+  type        = string
+  default     = ""
+}
diff --git a/services/terraform/self-host/webapp.tf b/services/terraform/self-host/webapp.tf
--- a/services/terraform/self-host/webapp.tf
+++ b/services/terraform/self-host/webapp.tf
@@ -12,178 +12,21 @@
       "COMM_NODE_ROLE"                          = "webapp",
       "COMM_JSONCONFIG_facts_run_server_config" = local.webapp_run_server_config
   })
-
-  webapp_environment = [
-    for name, value in local.webapp_environment_vars : {
-      name  = name
-      value = value
-    }
-  ]
-}
-
-resource "aws_cloudwatch_log_group" "webapp_service" {
-  name              = "/ecs/webapp-task-def"
-  retention_in_days = 7
-}
-
-resource "aws_ecs_task_definition" "webapp_service" {
-  network_mode             = "awsvpc"
-  family                   = "webapp-task-def"
-  requires_compatibilities = ["FARGATE"]
-  task_role_arn            = aws_iam_role.ecs_task_role.arn
-  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
-  cpu                      = "2048"
-  memory                   = "4096"
-
-  ephemeral_storage {
-    size_in_gib = 40
-  }
-
-  container_definitions = jsonencode([
-    {
-      name      = local.webapp_container_name
-      image     = local.keyserver_service_server_image
-      essential = true
-      portMappings = [
-        {
-          name          = "webapp-port"
-          containerPort = 3000
-          hostPort      = 3000,
-          protocol      = "tcp"
-        },
-
-      ]
-      environment = local.webapp_environment
-      logConfiguration = {
-        "logDriver" = "awslogs"
-        "options" = {
-          "awslogs-create-group"  = "true"
-          "awslogs-group"         = aws_cloudwatch_log_group.webapp_service.name
-          "awslogs-stream-prefix" = "ecs"
-          "awslogs-region"        = "${var.region}"
-        }
-      }
-      linuxParameters = {
-        initProcessEnabled = true
-      }
-    }
-  ])
-
-  runtime_platform {
-    cpu_architecture        = "ARM64"
-    operating_system_family = "LINUX"
-  }
-
-  skip_destroy = false
-}
-
-resource "aws_ecs_service" "webapp_service" {
-  depends_on = [null_resource.create_comm_database]
-
-  name                               = "webapp-service"
-  cluster                            = aws_ecs_cluster.keyserver_cluster.id
-  task_definition                    = aws_ecs_task_definition.webapp_service.arn
-  launch_type                        = "FARGATE"
-  enable_execute_command             = true
-  enable_ecs_managed_tags            = true
-  force_new_deployment               = true
-  desired_count                      = 2
-  deployment_maximum_percent         = 200
-  deployment_minimum_healthy_percent = 100
-
-
-  network_configuration {
-    subnets          = local.vpc_subnets
-    security_groups  = [aws_security_group.keyserver_service.id]
-    assign_public_ip = true
-  }
-
-  load_balancer {
-    target_group_arn = aws_lb_target_group.webapp_service.arn
-    container_name   = local.webapp_container_name
-    container_port   = 3000
-  }
-
-  deployment_circuit_breaker {
-    enable   = true
-    rollback = true
-  }
-}
-
-resource "aws_lb_target_group" "webapp_service" {
-  name     = "webapp-service-ecs-tg"
-  port     = 3000
-  protocol = "HTTP"
-  vpc_id   = local.vpc_id
-
-  # "awsvpc" network mode requires target type set to ip
-  target_type = "ip"
-
-  stickiness {
-    type            = "lb_cookie"
-    cookie_duration = 86500
-    enabled         = true
-  }
-
-  health_check {
-    enabled             = true
-    healthy_threshold   = 2
-    unhealthy_threshold = 3
-
-    protocol = "HTTP"
-    path     = "/health"
-    matcher  = "200-299"
-  }
-}
-
-resource "aws_lb" "webapp_service" {
-  load_balancer_type = "application"
-  name               = "webapp-service-lb"
-  security_groups    = [aws_security_group.webapp_lb_sg.id]
-
-  internal = false
-  subnets  = local.vpc_subnets
-}
-
-resource "aws_lb_listener" "webapp_service" {
-  load_balancer_arn = aws_lb.webapp_service.arn
-  port              = "443"
-  protocol          = "HTTPS"
-  ssl_policy        = "ELBSecurityPolicy-2016-08"
-  certificate_arn   = data.aws_acm_certificate.webapp_service.arn
-
-  default_action {
-    type             = "forward"
-    target_group_arn = aws_lb_target_group.webapp_service.arn
-  }
-
-  lifecycle {
-    ignore_changes       = [default_action[0].forward[0].stickiness[0].duration]
-    replace_triggered_by = [aws_lb_target_group.webapp_service]
-  }
-}
-
-resource "aws_security_group" "webapp_lb_sg" {
-  name        = "web-lb-sg"
-  description = "Security group for webapp load balancer"
-  vpc_id      = local.vpc_id
-
-  ingress {
-    from_port   = 443
-    to_port     = 443
-    protocol    = "tcp"
-    cidr_blocks = ["0.0.0.0/0"]
-  }
-
-  egress {
-    from_port   = 0
-    to_port     = 0
-    protocol    = "-1"
-    cidr_blocks = ["0.0.0.0/0"]
-  }
 }
 
-data "aws_acm_certificate" "webapp_service" {
-  domain   = var.webapp_domain_name
-  statuses = ["ISSUED"]
+module "webapp_service" {
+  source = "../modules/keyserver_node_service"
+  count  = var.enable_webapp_service ? 1 : 0
+
+  container_name              = "webapp"
+  image                       = local.keyserver_service_server_image
+  service_name                = "webapp"
+  cluster_id                  = aws_ecs_cluster.keyserver_cluster.id
+  domain_name                 = var.webapp_domain_name
+  vpc_id                      = local.vpc_id
+  vpc_subnets                 = local.vpc_subnets
+  region                      = var.region
+  environment_vars            = local.webapp_environment_vars
+  ecs_task_role_arn           = aws_iam_role.ecs_task_role.arn
+  ecs_task_execution_role_arn = aws_iam_role.ecs_task_execution.arn
 }