Skip to content
23 changes: 23 additions & 0 deletions modules/aws_ecs_fargate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AWS ECS + Fargate Module

This module deploys and ECS cluster with AWS Fargate. This eliminates the need to manually provision, scale and manage compute instances.

# Prerequisites

- A VPC
- with 2+ private and 2+ public subnets,
- and a route from the private subnets to the Internet (perhaps via a NAT gateway with EIP)

# Key Design Decisions

The module will deploy an RDS instance in the same VPC as the Fargate cluster.

# Usage

1. Directly use our module in your existing Terraform configuration and provide the required variables

```
module "retool" {
...
}
```
53 changes: 53 additions & 0 deletions modules/aws_ecs_fargate/loadbalancers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
resource "aws_lb" "this" {
name = "${var.deployment_name}-alb"
idle_timeout = var.alb_idle_timeout

security_groups = [aws_security_group.alb.id]
subnets = var.alb_subnet_ids
}

resource "aws_lb_listener" "this" {
load_balancer_arn = aws_lb.this.arn
port = 80
protocol = "HTTP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}
}

resource "aws_lb_listener_rule" "this" {
listener_arn = aws_lb_listener.this.arn
priority = 1

action {
type = "forward"
target_group_arn = aws_lb_target_group.this.arn
}

condition {
path_pattern {
values = ["/"]
}
}
}

resource "aws_lb_target_group" "this" {
name = "${var.deployment_name}-target"
vpc_id = var.vpc_id
deregistration_delay = 30
port = 80
protocol = "HTTP"
target_type = "ip"


health_check {
interval = 10
path = "/api/checkHealth"
protocol = "HTTP"
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 2
}
}
72 changes: 72 additions & 0 deletions modules/aws_ecs_fargate/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
locals {
environment_variables = concat(
var.additional_env_vars, # add additional environment variables
[
{
name = "NODE_ENV"
value = var.node_env
},
{
name = "FORCE_DEPLOYMENT"
value = tostring(var.force_deployment)
},
{
name = "POSTGRES_DB"
value = "hammerhead_production"
},
{
name = "POSTGRES_HOST"
value = aws_db_instance.this.address
},
{
name = "POSTGRES_SSL_ENABLED"
value = "true"
},
{
name = "POSTGRES_PORT"
value = "5432"
},
{
"name" = "POSTGRES_USER",
"value" = var.rds_username
},
{
"name" = "POSTGRES_PASSWORD",
"value" = random_string.rds_password.result
},
{
"name" : "JWT_SECRET",
"value" : random_string.jwt_secret.result
},
{
"name" : "ENCRYPTION_KEY",
"value" : random_string.encryption_key.result
},
{
"name" : "LICENSE_KEY",
"value" : var.retool_license_key
}
]
)

stack_name = "${var.deployment_name}"
database_name = aws_db_instance.this.db_name
db_subnet_group_name = "${var.deployment_name}-subnet-group"
retool_image = "${var.ecs_retool_image}"
retool_alb_ingress_port = var.alb_listener_certificate_arn != null ? "443" : var.retool_alb_ingress_port
retool_alb_listener_protocol = var.alb_listener_certificate_arn != null ? "HTTPS" : var.aws_lb_listener_protocol
retool_alb_listener_ssl_policy = var.alb_listener_certificate_arn != null ? var.alb_listener_ssl_policy : null
retool_alb_listener_certificate_arn = var.alb_listener_certificate_arn
retool_url_port = local.retool_alb_ingress_port != "443" ? ":${local.retool_alb_ingress_port}" : ""

retool_jwt_secret = {
password = aws_secretsmanager_secret_version.jwt_secret
}
retool_encryption_key_secret = {
password = random_string.encryption_key.result
}
retool_rds_secret = {
username = "retool"
password = aws_secretsmanager_secret.rds_password
}
}
240 changes: 240 additions & 0 deletions modules/aws_ecs_fargate/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}

provider "aws" {
region = var.aws_region
}

resource "aws_ecs_cluster" "this" {
name = "${var.deployment_name}-ecs"

setting {
name = "containerInsights"
value = var.ecs_insights_enabled
}
}

resource "aws_cloudwatch_log_group" "this" {
name = "${var.deployment_name}-ecs-log-group"
retention_in_days = var.log_retention_in_days
}

resource "aws_db_subnet_group" "this" {
name = local.db_subnet_group_name
subnet_ids = var.rds_subnet_ids
}

resource "aws_db_instance" "this" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should allow people to opt-out of creating a DB with some var and then support the customer passing in the db information through env vars as normal

identifier = "${var.deployment_name}-rds-instance"
allocated_storage = 80
instance_class = var.rds_instance_class
engine = "postgres"
engine_version = "13.7"
db_name = "hammerhead_production"
username = aws_secretsmanager_secret_version.rds_username.secret_string
password = aws_secretsmanager_secret_version.rds_password.secret_string
port = 5432
publicly_accessible = var.rds_publicly_accessible
db_subnet_group_name = local.db_subnet_group_name
vpc_security_group_ids = [aws_security_group.rds.id]
performance_insights_enabled = var.rds_performance_insights_enabled

skip_final_snapshot = true
apply_immediately = true

depends_on = [
aws_db_subnet_group.this
]
}

resource "aws_ecs_service" "retool" {
name = "${var.deployment_name}-main-service"
cluster = aws_ecs_cluster.this.id
task_definition = aws_ecs_task_definition.retool.arn
desired_count = var.ecs_service_count
deployment_maximum_percent = var.maximum_percent
deployment_minimum_healthy_percent = var.minimum_healthy_percent
launch_type = "FARGATE"

load_balancer {
target_group_arn = aws_lb_target_group.this.arn
container_name = "retool"
container_port = 3000
}

network_configuration {
subnets = var.ecs_tasks_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
}
}

resource "aws_ecs_task_definition" "retool" {
family = "${var.deployment_name}-backend"
requires_compatibilities = ["FARGATE"]
network_mode = var.ecs_task_network_mode
cpu = var.ecs_task_cpu
memory = var.ecs_task_memory
task_role_arn = aws_iam_role.task_role.arn
execution_role_arn = aws_iam_role.execution_role.arn
container_definitions = <<TASK_DEFINITION
[
{
"command": ["./docker_scripts/start_api.sh"],
"environment": [
{"name": "NODE_ENV", "value": "${var.node_env}"},
{"name": "SERVICE_TYPE", "value": "MAIN_BACKEND,DB_CONNECTOR"},
{"name": "FORCE_DEPLOYMENT", "value": "${var.force_deployment}"},
{"name": "POSTGRES_DB", "value": "hammerhead_production"},
{"name": "POSTGRES_HOST", "value": "${aws_db_instance.this.address}"},
{"name": "POSTGRES_SSL_ENABLED", "value": "${var.postgresql_ssl_enabled}"},
{"name": "POSTGRES_PORT", "value": "${var.postgresql_db_port}"},
{"name": "POSTGRES_USER", "value": "${aws_secretsmanager_secret_version.rds_username.secret_string}"},
{"name": "POSTGRES_PASSWORD", "value": "${aws_secretsmanager_secret_version.rds_password.secret_string}"},
{"name": "JWT_SECRET", "value": "${random_string.jwt_secret.result}"},
{"name": "ENCRYPTION_KEY", "value": "${random_string.encryption_key.result}"},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i always am weary about randomly generating keys like this because if a customer were to want to keep their db but for some reason re-create their retool containers, they may get a new encryption key generated and nothing will work right :')

typically this stuff should be customer-provided and referenced through aws secrets manager in this case

{"name": "LICENSE_KEY", "value": "${var.retool_license_key}"},
{"name": "COOKIE_INSECURE", "value": "${var.cookie_insecure}"}
],
"logConfiguration": {
"logDriver": "${var.retool_ecs_tasks_logdriver}",
"options": {
"awslogs-group": "${aws_cloudwatch_log_group.this.name}",
"awslogs-region": "${var.aws_region}",
"awslogs-stream-prefix": "${var.retool_ecs_tasks_log_prefix}"
}
},
"essential": true,
"image": "${local.retool_image}",
"name": "${var.retool_task_container_name}",
"portMappings": [
{
"containerPort": ${var.retool_task_container_port}
}
]
}
]
TASK_DEFINITION
}

resource "aws_ecs_service" "jobs_runner" {
name = "${var.deployment_name}-jobs-runner-service"
cluster = aws_ecs_cluster.this.id
desired_count = 1
task_definition = aws_ecs_task_definition.retool_jobs_runner.arn
launch_type = "FARGATE"

network_configuration {
subnets = var.ecs_tasks_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
}
}

resource "aws_ecs_task_definition" "retool_jobs_runner" {
family = "${var.deployment_name}-jobs-runner"
requires_compatibilities = ["FARGATE"]
network_mode = var.ecs_task_network_mode
cpu = var.ecs_task_cpu
memory = var.ecs_task_memory
task_role_arn = aws_iam_role.task_role.arn
execution_role_arn = aws_iam_role.execution_role.arn
container_definitions = <<TASK_DEFINITION
[
{
"command": ["./docker_scripts/start_api.sh"],
"environment": [
{"name": "NODE_ENV", "value": "${var.node_env}"},
{"name": "SERVICE_TYPE", "value": "JOBS_RUNNER"},
{"name": "FORCE_DEPLOYMENT", "value": "${var.force_deployment}"},
{"name": "POSTGRES_DB", "value": "hammerhead_production"},
{"name": "POSTGRES_HOST", "value": "${aws_db_instance.this.address}"},
{"name": "POSTGRES_SSL_ENABLED", "value": "${var.postgresql_ssl_enabled}"},
{"name": "POSTGRES_PORT", "value": "${var.postgresql_db_port}"},
{"name": "POSTGRES_USER", "value": "${aws_secretsmanager_secret_version.rds_username.secret_string}"},
{"name": "POSTGRES_PASSWORD", "value": "${aws_secretsmanager_secret_version.rds_password.secret_string}"},
{"name": "JWT_SECRET", "value": "${random_string.jwt_secret.result}"},
{"name": "ENCRYPTION_KEY", "value": "${random_string.encryption_key.result}"},
{"name": "LICENSE_KEY", "value": "${var.retool_license_key}"},
{"name": "COOKIE_INSECURE", "value": "${var.cookie_insecure}"}
],
"logConfiguration": {
"logDriver": "${var.retool_ecs_tasks_logdriver}",
"options": {
"awslogs-group": "${aws_cloudwatch_log_group.this.name}",
"awslogs-region": "${var.aws_region}",
"awslogs-stream-prefix": "${var.retool_ecs_tasks_log_prefix}"
}
},
"essential": true,
"image": "${local.retool_image}",
"name": "${var.retool_task_container_name}",
"portMappings": [
{
"containerPort": ${var.retool_task_container_port}
}
]
}
]
TASK_DEFINITION
}

# resource "aws_iam_role" "retool_service_role" {
# name = "${var.deployment_name}-service-role"
# path = "/"
# assume_role_policy = jsonencode({
# Version = "2012-10-17"
# Statement = [
# {
# Action = "sts:AssumeRole"
# Effect = "Allow"
# Principal = {
# Service = "ecs.amazonaws.com"
# }
# }
# ]
# })

# inline_policy {
# name = "${var.deployment_name}-env-service-policy"

# policy = jsonencode({
# Version = "2012-10-17"
# Statement = [
# {
# Action = [
# "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
# "elasticloadbalancing:DeregisterTargets",
# "elasticloadbalancing:Describe*",
# "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
# "elasticloadbalancing:RegisterTargets",
# "ec2:Describe*",
# "ec2:AuthorizeSecurityGroupIngress"
# ]
# Effect = "Allow"
# Resource = "*"
# }]
# })
# }
# }

# resource "aws_iam_role" "retool_task_role" {
# name = "${var.deployment_name}-task-role"
# path = "/"
# assume_role_policy = jsonencode({
# Version = "2012-10-17"
# Statement = [
# {
# Action = "sts:AssumeRole"
# Effect = "Allow"
# Principal = {
# Service = "ecs-tasks.amazonaws.com"
# }
# }
# ]
# })
# }
Loading