diff --git a/deploy/tf/cluster.tf b/deploy/tf/cluster.tf new file mode 100644 index 0000000..3a7edaa --- /dev/null +++ b/deploy/tf/cluster.tf @@ -0,0 +1,26 @@ +module "cluster" { + source = "terraform-aws-modules/ecs/aws//modules/cluster" + version = "~> 5.7.0" + + create = var.create_cluster ? true : false + + # Cluster Configuration + cluster_name = "${var.app_name}-${var.environment}" + cluster_configuration = { + name = "containerInsights" + value = "enabled" + } + create_task_exec_iam_role = true + fargate_capacity_providers = { + FARGATE = { + default_capacity_provider_strategy = { + weight = 50 + } + } + FARGATE_SPOT = { + default_capacity_provider_strategy = { + weight = 50 + } + } + } +} diff --git a/deploy/tf/ecs.tf b/deploy/tf/ecs.tf deleted file mode 100644 index 6750d12..0000000 --- a/deploy/tf/ecs.tf +++ /dev/null @@ -1,191 +0,0 @@ -locals { - nginx_vars = { - app_host = "127.0.0.1" - app_port = var.app_port - listen_port = var.proxy_port - } - - app_container_vars = [for k, v in var.container_vars : { name = upper(k), value = v }] - nginx_container_vars = [for k, v in local.nginx_vars : { name = upper(k), value = v }] - - container_definitions = var.nginx_proxy ? merge(local.app_container_definition, local.nginx_container_definition) : local.app_container_definition - app_container_definition = { - app = { - name = var.app_container_name - image = ( - startswith(var.image, "sha256") ? - "${var.ecr_registry}/${var.ecr_repository}@${var.image}" : - "${var.ecr_registry}/${var.ecr_repository}:${var.image}" - ) - health_check = { - command = ["CMD-SHELL", "uwsgi-is-ready --stats-socket /tmp/statsock > /dev/null 2>&1 || exit 1"] - } - readonly_root_filesystem = false - essential = true - memory_reservation = 256 - environment = local.app_container_vars - port_mappings = [ - { - name = var.app_container_name - containerPort = var.app_port - hostPort = var.app_port - } - ] - mount_points = [ - { - readOnly = false - containerPath = "/vol/web" - sourceVolume = "static" - } - ] - } - } - nginx_container_definition = { - nginx = { - name = "nginx" - image = "${var.ecr_registry}/nginx-proxy:latest" - health_check = { - command = ["CMD-SHELL", "curl -so /dev/null http://localhost/health || exit 1"] - } - readonly_root_filesystem = false - essential = true - memory_reservation = 256 - environment = local.nginx_container_vars - port_mappings = [ - { - name = "nginx" - containerPort = var.proxy_port - hostPort = var.proxy_port - } - ] - mount_points = [ - { - readOnly = false - containerPath = "/vol/static" - sourceVolume = "static" - } - ] - } - } -} - -module "ecs" { - source = "terraform-aws-modules/ecs/aws" - version = "~> 5.7.0" - - depends_on = [module.s3.wrapper] - - # Cluster Configuration - cluster_name = "${var.app_name}-${var.environment}" - cluster_configuration = { - name = "containerInsights" - value = "enabled" - } - create_task_exec_iam_role = true - fargate_capacity_providers = { - FARGATE = { - default_capacity_provider_strategy = { - weight = 50 - } - } - FARGATE_SPOT = { - default_capacity_provider_strategy = { - weight = 50 - } - } - } - - # Service Configuration - services = { - - "${var.app_name}-${var.environment}" = { - capacity_provider_strategy = { - env_strategy = { - base = 0 - capacity_provider = var.environment == "production" ? "FARGATE" : "FARGATE_SPOT" - weight = 100 - } - } - - # allow ECS exec commands on containers (e.g. to get a shell session) - enable_execute_command = true - - # resources - cpu = var.cpu - memory = var.memory - - # do not force a new deployment unless the image digest has changed - force_new_deployment = false - - # wait for service to reach steady state - wait_for_steady_state = true - - # Container definition(s) - container_definitions = local.container_definitions - - deployment_circuit_breaker = { - enable = true - rollback = true - } - - load_balancer = { - service = { - target_group_arn = aws_lb_target_group.app.arn - container_name = var.nginx_proxy ? "nginx" : "app" - container_port = var.nginx_proxy ? var.proxy_port : var.app_port - } - } - - subnet_ids = local.private_subnets - - security_group_rules = { - ingress_vpc = { - type = "ingress" - from_port = var.nginx_proxy ? var.proxy_port : var.app_port - to_port = var.nginx_proxy ? var.proxy_port : var.app_port - protocol = "tcp" - cidr_blocks = [local.vpc_cidr] - } - egress_all = { - type = "egress" - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - } - - tasks_iam_role_statements = [ - { - actions = [ - "s3:PutObject", - "s3:GetObjectAcl", - "s3:GetObject", - "s3:ListBucket", - "s3:DeleteObject", - "s3:PutObjectAcl" - ] - resources = flatten([for bucket in module.s3.wrapper : - split(",", "arn:aws:s3:::${bucket.s3_bucket_id},arn:aws:s3:::${bucket.s3_bucket_id}/*" - )]) - }, - { - actions = [ - "secretsmanager:GetSecretValue" - ] - resources = ["arn:aws:secretsmanager:${data.aws_region.current.name}:*:secret:/rds*"] - } - ] - - timeouts = { - create = "10m" - update = "5m" - delete = "10m" - } - - volume = { - static = {} - } - } - } -} diff --git a/deploy/tf/outputs.tf b/deploy/tf/outputs.tf index ccef840..2c7aa1e 100644 --- a/deploy/tf/outputs.tf +++ b/deploy/tf/outputs.tf @@ -2,6 +2,10 @@ output "buckets" { value = module.s3.wrapper } -output "ecs" { - value = module.ecs +output "cluster" { + value = module.cluster +} + +output "service" { + value = module.service } diff --git a/deploy/tf/service.tf b/deploy/tf/service.tf new file mode 100644 index 0000000..15e3475 --- /dev/null +++ b/deploy/tf/service.tf @@ -0,0 +1,178 @@ +locals { + nginx_vars = { + app_host = "127.0.0.1" + app_port = var.app_port + listen_port = var.proxy_port + } + + app_container_vars = [for k, v in var.container_vars : { name = upper(k), value = v }] + nginx_container_vars = [for k, v in local.nginx_vars : { name = upper(k), value = v }] + + container_definitions = var.nginx_proxy ? merge(local.app_container_definition, local.nginx_container_definition) : local.app_container_definition + app_container_definition = { + app = { + name = var.app_container_name + image = ( + startswith(var.image, "sha256") ? + "${var.ecr_registry}/${var.ecr_repository}@${var.image}" : + "${var.ecr_registry}/${var.ecr_repository}:${var.image}" + ) + health_check = { + command = ["CMD-SHELL", "uwsgi-is-ready --stats-socket /tmp/statsock > /dev/null 2>&1 || exit 1"] + } + readonly_root_filesystem = false + essential = true + memory_reservation = 256 + environment = local.app_container_vars + port_mappings = [ + { + name = var.app_container_name + containerPort = var.app_port + hostPort = var.app_port + } + ] + mount_points = [ + { + readOnly = false + containerPath = "/vol/web" + sourceVolume = "static" + } + ] + } + } + nginx_container_definition = { + nginx = { + name = "nginx" + image = "${var.ecr_registry}/nginx-proxy:latest" + health_check = { + command = ["CMD-SHELL", "curl -so /dev/null http://localhost/health || exit 1"] + } + readonly_root_filesystem = false + essential = true + memory_reservation = 256 + environment = local.nginx_container_vars + port_mappings = [ + { + name = "nginx" + containerPort = var.proxy_port + hostPort = var.proxy_port + } + ] + mount_points = [ + { + readOnly = false + containerPath = "/vol/static" + sourceVolume = "static" + } + ] + } + } +} + +resource "null_resource" "cluster_arn_precondition_check" { + lifecycle { + precondition { + condition = (var.create_cluster == false && var.existing_cluster_arn != "" || var.create_cluster && var.existing_cluster_arn == "") + error_message = "The cluster ARN must be provided if 'create_cluster' is false. If you mean to create the cluster, set 'create_cluster' to true." + } + } +} + +module "service" { + source = "terraform-aws-modules/ecs/aws//modules/service" + version = "~> 5.7.0" + + depends_on = [module.s3.wrapper] + + name = "${var.app_name}-${var.environment}" + cluster_arn = var.create_cluster ? module.cluster.arn : var.existing_cluster_arn + capacity_provider_strategy = { + env_strategy = { + base = 0 + capacity_provider = var.environment == "production" ? "FARGATE" : "FARGATE_SPOT" + weight = 100 + } + } + + # allow ECS exec commands on containers (e.g. to get a shell session) + enable_execute_command = true + + # resources + cpu = var.cpu + memory = var.memory + + # do not force a new deployment unless the image digest has changed + force_new_deployment = false + + # wait for service to reach steady state + wait_for_steady_state = true + + # wait for the task to reach steady state + wait_until_stable = true + + # Container definition(s) + container_definitions = local.container_definitions + + deployment_circuit_breaker = { + enable = true + rollback = true + } + + load_balancer = { + service = { + target_group_arn = aws_lb_target_group.app.arn + container_name = var.nginx_proxy ? "nginx" : "app" + container_port = var.nginx_proxy ? var.proxy_port : var.app_port + } + } + + subnet_ids = local.private_subnets + + security_group_rules = { + ingress_vpc = { + type = "ingress" + from_port = var.nginx_proxy ? var.proxy_port : var.app_port + to_port = var.nginx_proxy ? var.proxy_port : var.app_port + protocol = "tcp" + cidr_blocks = [local.vpc_cidr] + } + egress_all = { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + } + + tasks_iam_role_statements = [ + { + actions = [ + "s3:PutObject", + "s3:GetObjectAcl", + "s3:GetObject", + "s3:ListBucket", + "s3:DeleteObject", + "s3:PutObjectAcl" + ] + resources = flatten([for bucket in module.s3.wrapper : + split(",", "arn:aws:s3:::${bucket.s3_bucket_id},arn:aws:s3:::${bucket.s3_bucket_id}/*" + )]) + }, + { + actions = [ + "secretsmanager:GetSecretValue" + ] + resources = ["arn:aws:secretsmanager:${data.aws_region.current.name}:*:secret:/rds*"] + } + ] + + timeouts = { + create = "10m" + update = "10m" + } + + volume = { + static = {} + } +} diff --git a/deploy/tf/variables.tf b/deploy/tf/variables.tf index deee518..1c0ac02 100644 --- a/deploy/tf/variables.tf +++ b/deploy/tf/variables.tf @@ -25,6 +25,12 @@ variable "app_hostnames" { type = list(string) } +variable "existing_cluster_arn" { + description = "ARN of the existing cluster to deploy the service/tasks to." + type = string + default = "" +} + variable "container_vars" { description = "Map of key/pair values to pass to the container definition." type = map(any) @@ -36,6 +42,12 @@ variable "cpu" { default = 512 } +variable "create_cluster" { + description = "Whether or not to create a separate cluster for this deployment. If false, the name of an existing cluster must be provided." + type = bool + default = false +} + variable "ecr_registry" { description = "The registry to pull docker images from." type = string diff --git a/deploy/tg/terragrunt.hcl b/deploy/tg/terragrunt.hcl index 21f81b0..3d10f30 100644 --- a/deploy/tg/terragrunt.hcl +++ b/deploy/tg/terragrunt.hcl @@ -4,8 +4,9 @@ include "global" { } inputs = { - app_name = get_env("APP_NAME") - environment = local.global.environment + app_name = get_env("APP_NAME") + create_cluster = true + environment = local.global.environment # fetch the ssm parameter names alb_parameter_name = get_env("ALB_PARAMETER_NAME")