diff --git a/.happy/config.json b/.happy/config.json index 5be69cd8a..738a38ab9 100644 --- a/.happy/config.json +++ b/.happy/config.json @@ -16,8 +16,16 @@ "secret_arn": "happy/env-explorer-dev-config", "terraform_directory": ".happy/terraform/envs/dev", "delete_protected": false, - "auto_run_migrations": true, + "auto_run_migrations": false, "log_group_prefix": "/explorer/dev" + }, + "stage": { + "aws_profile": "single-cell-dev", + "secret_arn": "happy/env-explorer-staging-config", + "terraform_directory": ".happy/terraform/envs/staging", + "delete_protected": false, + "auto_run_migrations": false, + "log_group_prefix": "/explorer/staging" } }, "tasks": {} diff --git a/.happy/terraform/envs/staging/main.tf b/.happy/terraform/envs/staging/main.tf new file mode 100644 index 000000000..135462ad8 --- /dev/null +++ b/.happy/terraform/envs/staging/main.tf @@ -0,0 +1,18 @@ +module stack { + source = "./modules/ecs-stack" + aws_account_id = var.aws_account_id + aws_role = var.aws_role + happymeta_ = var.happymeta_ + happy_config_secret = var.happy_config_secret + image_tag = var.image_tag + priority = var.priority + stack_name = var.stack_name + deployment_stage = "staging" + delete_protected = false + require_okta = false + stack_prefix = "/${var.stack_name}" + batch_container_memory_limit = 28000 + memory = 50000 + + wait_for_steady_state = var.wait_for_steady_state +} diff --git a/.happy/terraform/envs/staging/modules/dns/main.tf b/.happy/terraform/envs/staging/modules/dns/main.tf new file mode 100644 index 000000000..05b8693df --- /dev/null +++ b/.happy/terraform/envs/staging/modules/dns/main.tf @@ -0,0 +1,17 @@ +# This template creates a route53 cname for a shared alb resource. +# + +data aws_route53_zone dns_record { + name = var.zone +} + +resource aws_route53_record dns_record_0 { + name = "${var.custom_stack_name}-${var.app_name}.${var.zone}" + type = "A" + zone_id = data.aws_route53_zone.dns_record.zone_id + alias { + name = var.alb_dns + zone_id = var.canonical_hosted_zone + evaluate_target_health = false + } +} diff --git a/.happy/terraform/envs/staging/modules/dns/outputs.tf b/.happy/terraform/envs/staging/modules/dns/outputs.tf new file mode 100644 index 000000000..79de6a032 --- /dev/null +++ b/.happy/terraform/envs/staging/modules/dns/outputs.tf @@ -0,0 +1,4 @@ +output dns_prefix { + value = "${var.custom_stack_name}-${var.app_name}" + description = "User-facing URL for this service." +} diff --git a/.happy/terraform/envs/staging/modules/dns/variables.tf b/.happy/terraform/envs/staging/modules/dns/variables.tf new file mode 100644 index 000000000..e19bc6778 --- /dev/null +++ b/.happy/terraform/envs/staging/modules/dns/variables.tf @@ -0,0 +1,24 @@ +variable custom_stack_name { + type = string + description = "Please provide the stack name" +} + +variable app_name { + type = string + description = "Please provide the ECS service name" +} + +variable zone { + type = string + description = "Route53 zone name. Trailing . must be OMITTED!" +} + +variable alb_dns { + type = string + description = "DNS name for the shared ALB" +} + +variable canonical_hosted_zone { + type = string + description = "Route53 zone for the shared ALB" +} diff --git a/.happy/terraform/envs/staging/modules/ecs-stack/main.tf b/.happy/terraform/envs/staging/modules/ecs-stack/main.tf new file mode 100644 index 000000000..58601c09b --- /dev/null +++ b/.happy/terraform/envs/staging/modules/ecs-stack/main.tf @@ -0,0 +1,79 @@ +# This deploys an Explorer stack. +# + +data aws_secretsmanager_secret_version config { + secret_id = var.happy_config_secret +} + +locals { + secret = jsondecode(data.aws_secretsmanager_secret_version.config.secret_string) + alb_key = var.require_okta ? "private_albs" : "public_albs" + + custom_stack_name = var.stack_name + image_tag = var.image_tag + priority = var.priority + deployment_stage = var.deployment_stage + remote_dev_prefix = var.stack_prefix + wait_for_steady_state = var.wait_for_steady_state + + vpc_id = local.secret["vpc_id"] + subnets = local.secret["private_subnets"] + security_groups = local.secret["security_groups"] + zone = local.secret["zone_id"] + cluster = local.secret["cluster_arn"] + external_dns = local.secret["external_zone_name"] + internal_dns = local.secret["internal_zone_name"] + + explorer_listener_arn = try(local.secret[local.alb_key]["explorer"]["listener_arn"], "") + explorer_alb_dns = try(local.secret[local.alb_key]["explorer"]["dns_name"], "") + explorer_alb_zone = try(local.secret[local.alb_key]["explorer"]["zone_id"], "") + + frontend_url = try(join("", [ + "https://", module.explorer_dns[0].dns_prefix, ".", local.external_dns + ]), var.frontend_url) + explorer_image_repo = local.secret["ecrs"]["explorer"]["url"] + explorer_cmd = ["gunicorn", "--worker-class", "gevent", "--bind", "0.0.0.0:5000", "server.eb.app:application", "--timeout", "60"] + # TODO end explorer stuff + + artifact_bucket = try(local.secret["s3_buckets"]["artifact"]["name"], "") + cellxgene_bucket = try(local.secret["s3_buckets"]["cellxgene"]["name"], "") + + ecs_role_arn = local.secret["service_roles"]["ecs_role"] + +} + +module explorer_dns { + count = var.require_okta ? 1 : 0 + source = "../dns" + custom_stack_name = local.custom_stack_name + app_name = "explorer" + alb_dns = local.explorer_alb_dns + canonical_hosted_zone = local.explorer_alb_zone + zone = local.internal_dns +} + +module explorer_service { + source = "../service" + custom_stack_name = local.custom_stack_name + app_name = "explorer" + vpc = local.vpc_id + image = "${local.explorer_image_repo}:${local.image_tag}" + cluster = local.cluster + desired_count = var.explorer_instance_count + listener = local.explorer_listener_arn + subnets = local.subnets + security_groups = local.security_groups + task_role_arn = local.ecs_role_arn + service_port = 5000 + memory = var.memory + cmd = local.explorer_cmd + deployment_stage = local.deployment_stage + health_check_path = "/cellxgene/health" + host_match = try(join(".", [module.explorer_dns[0].dns_prefix, local.external_dns]), "") + priority = local.priority + api_url = local.frontend_url + frontend_url = local.frontend_url + remote_dev_prefix = local.remote_dev_prefix + + wait_for_steady_state = local.wait_for_steady_state +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/modules/ecs-stack/outputs.tf b/.happy/terraform/envs/staging/modules/ecs-stack/outputs.tf new file mode 100644 index 000000000..a1aa6a05b --- /dev/null +++ b/.happy/terraform/envs/staging/modules/ecs-stack/outputs.tf @@ -0,0 +1,4 @@ +output frontend_url { + value = local.frontend_url + description = "The URL endpoint for the website service" +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/modules/ecs-stack/variables.tf b/.happy/terraform/envs/staging/modules/ecs-stack/variables.tf new file mode 100644 index 000000000..f056fa7fc --- /dev/null +++ b/.happy/terraform/envs/staging/modules/ecs-stack/variables.tf @@ -0,0 +1,89 @@ +variable aws_account_id { + type = string + description = "AWS account ID to apply changes to" + default = "" +} + +variable aws_role { + type = string + description = "Name of the AWS role to assume to apply changes" + default = "" +} + +variable image_tag { + type = string + description = "Please provide an image tag" +} + +variable priority { + type = number + description = "Listener rule priority number within the given listener" +} + +variable happymeta_ { + type = string + description = "Happy Path metadata. Ignored by actual terraform." +} + +variable stack_name { + type = string + description = "Happy Path stack name" +} + +variable happy_config_secret { + type = string + description = "Happy Path configuration secret name" +} + +variable deployment_stage { + type = string + description = "Deployment stage for the app" +} + +variable delete_protected { + type = bool + description = "Whether to protect this stack from being deleted." + default = false +} + +variable require_okta { + type = bool + description = "Whether the ALB's should be on private subnets" + default = true +} + +variable stack_prefix { + type = string + description = "Do bucket storage paths and db schemas need to be prefixed with the stack name? (Usually '/{stack_name}' for dev stacks, and '' for staging/prod stacks)" + default = "" +} + +variable wait_for_steady_state { + type = bool + description = "Should terraform block until ECS services reach a steady state?" + default = false +} + +variable batch_container_memory_limit { + type = number + description = "Memory hard limit for the batch container" + default = 28000 +} + +variable frontend_url { + type = string + description = "For non-proxied stacks, send in the canonical front/backend URL's" + default = "" +} + +variable explorer_instance_count { + type = number + description = "How many backend tasks to run" + default = 1 +} + +variable memory { + type = number + description = "Allocated memory" + default = 1536 +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/modules/service/main.tf b/.happy/terraform/envs/staging/modules/service/main.tf new file mode 100644 index 000000000..63a0b416f --- /dev/null +++ b/.happy/terraform/envs/staging/modules/service/main.tf @@ -0,0 +1,147 @@ +# This is a service managed by ECS attached to the environment's load balancer +# + +data aws_region current {} + +resource aws_ecs_service service { + cluster = var.cluster + desired_count = var.desired_count + task_definition = aws_ecs_task_definition.task_definition.id + launch_type = "EC2" + name = "${var.custom_stack_name}-${var.app_name}" + load_balancer { + container_name = "web" + container_port = var.service_port + target_group_arn = aws_lb_target_group.target_group.id + } + network_configuration { + security_groups = var.security_groups + subnets = var.subnets + assign_public_ip = false + } + + wait_for_steady_state = var.wait_for_steady_state +} + +resource aws_ecs_task_definition task_definition { + family = "explorer-${var.deployment_stage}-${var.custom_stack_name}-${var.app_name}" + network_mode = "awsvpc" + task_role_arn = var.task_role_arn + container_definitions = < 0 ? 1 : 0 + # Dev stacks need to match on hostnames + condition { + # for_each = length(var.host_match_internal) == 0 ? [] : [var.host_match_internal] + host_header { + values = [ + var.host_match_internal + ] + } + } + + action { + target_group_arn = aws_lb_target_group.target_group.id + type = "forward" + } +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/modules/service/outputs.tf b/.happy/terraform/envs/staging/modules/service/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/.happy/terraform/envs/staging/modules/service/variables.tf b/.happy/terraform/envs/staging/modules/service/variables.tf new file mode 100644 index 000000000..aecd90aa8 --- /dev/null +++ b/.happy/terraform/envs/staging/modules/service/variables.tf @@ -0,0 +1,124 @@ +variable "vpc" { + type = string + description = "The VPC that the ECS cluster is deployed to" +} + +variable "custom_stack_name" { + type = string + description = "Please provide the stack name" +} + +variable "remote_dev_prefix" { + type = string + description = "S3 storage path / db schema prefix" + default = "" +} + +variable "app_name" { + type = string + description = "Please provide the ECS service name" +} + +variable "cluster" { + type = string + description = "Please provide the ECS Cluster ID that this service should run on" +} + +variable "image" { + type = string + description = "Image name" +} + +variable "service_port" { + type = number + description = "What port does this service run on?" + default = 80 +} + +variable "desired_count" { + type = number + description = "How many instances of this task should we run across our cluster?" + default = 2 +} + +variable "listener" { + type = string + description = "The Application Load Balancer listener to register with" +} + +variable "host_match" { + type = string + description = "Host header to match for target rule. Leave empty to match all requests" +} + +variable "host_match_internal" { + type = string + description = "Host header to match for target rule. Leave empty to match all requests" + default = "" +} + +variable "security_groups" { + type = list(string) + description = "Security groups for ECS tasks" +} + +variable "subnets" { + type = list(string) + description = "Subnets for ecs tasks" +} + +variable "task_role_arn" { + type = string + description = "ARN for the role assumed by tasks" +} + +variable "path" { + type = string + description = "The path to register with the Application Load Balancer" + default = "/*" +} + +variable "cmd" { + type = list(string) + description = "The path to register with the Application Load Balancer" + default = [] +} + +variable "api_url" { + type = string + description = "URL for the backend api." +} + +variable "deployment_stage" { + type = string + description = "The name of the deployment stage of the Application" + default = "test" +} + +variable "priority" { + type = number + description = "Listener rule priority number within the given listener" +} + +variable "memory" { + type = number + description = "Amount of memory to allocate to each task" + default = 2048 +} + +variable "wait_for_steady_state" { + type = bool + description = "Whether Terraform should block until the service is in a steady state before exiting" + default = false +} + +variable "health_check_path" { + type = string + description = "Path for health checks" + default = "/" +} + +variable "frontend_url" { + type = string + description = "URL for the frontend app." +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/outputs.tf b/.happy/terraform/envs/staging/outputs.tf new file mode 100644 index 000000000..3e02879b3 --- /dev/null +++ b/.happy/terraform/envs/staging/outputs.tf @@ -0,0 +1,4 @@ +output frontend_url { + value = module.stack.frontend_url + description = "The URL endpoint for the website service" +} \ No newline at end of file diff --git a/.happy/terraform/envs/staging/providers.tf b/.happy/terraform/envs/staging/providers.tf new file mode 100644 index 000000000..12a034a4e --- /dev/null +++ b/.happy/terraform/envs/staging/providers.tf @@ -0,0 +1,8 @@ +provider aws { + version = "~> 3.28.0" + region = "us-west-2" + assume_role { + role_arn = "arn:aws:iam::${var.aws_account_id}:role/${var.aws_role}" + } + allowed_account_ids = [var.aws_account_id] +} diff --git a/.happy/terraform/envs/staging/variables.tf b/.happy/terraform/envs/staging/variables.tf new file mode 100644 index 000000000..74187c821 --- /dev/null +++ b/.happy/terraform/envs/staging/variables.tf @@ -0,0 +1,42 @@ +variable aws_account_id { + type = string + description = "AWS account ID to apply changes to" + default = "" +} + +variable aws_role { + type = string + description = "Name of the AWS role to assume to apply changes" + default = "" +} + +variable image_tag { + type = string + description = "Please provide an image tag" +} + +variable priority { + type = number + description = "Listener rule priority number within the given listener" +} + +variable happymeta_ { + type = string + description = "Happy Path metadata. Ignored by actual terraform." +} + +variable stack_name { + type = string + description = "Happy Path stack name" +} + +variable happy_config_secret { + type = string + description = "Happy Path configuration secret name" +} + +variable wait_for_steady_state { + type = bool + description = "Should terraform block until ECS reaches a steady state?" + default = false +}