From 0e95b87eac803bec970135451d0d7bcc48721bc9 Mon Sep 17 00:00:00 2001 From: Judson Lester Date: Thu, 19 Oct 2023 15:13:03 -0700 Subject: [PATCH 1/4] ATD almost there --- documentation/terraform-add-new-project.md | 22 +++ terraform-incubator/access-the-data/main.tf | 73 +++++++ .../access-the-data/versions.tf | 58 ++++++ .../people-depot/project/main.tf | 2 +- .../shared_resources/acm/main.tf | 25 +++ .../shared_resources/alb/main.tf | 74 ++++++++ .../shared_resources/alb/moves.tf | 20 ++ .../shared_resources/all/main.tf | 57 +++++- .../shared_resources/rds/main.tf | 89 +++++++++ .../shared_resources/rds/moves.tf | 12 ++ terraform-modules/applicationlb/outputs.tf | 8 + terraform-modules/ecr/main.tf | 1 + terraform-modules/ecs-task/ecs_autoscaling.tf | 43 +++++ terraform-modules/ecs-task/main.tf | 179 ++++++++++++++++++ terraform-modules/ecs-task/variables.tf | 84 ++++++++ .../multi-container-service/database.tf | 42 ++++ .../multi-container-service/fargate.tf | 22 +++ .../multi-container-service/r53.tf | 13 ++ .../multi-container-service/tasks.tf | 53 ++++++ .../multi-container-service/tls.tf | 8 + .../multi-container-service/variables.tf | 86 +++++++++ terraform-modules/rds/main.tf | 1 + terraform-modules/rds/outputs.tf | 5 + terraform-modules/service/alb_resources.tf | 2 - terraform-modules/service/ecs_ec2.tf | 13 +- terraform-modules/service/task_definition.tf | 26 +-- 26 files changed, 987 insertions(+), 31 deletions(-) create mode 100644 documentation/terraform-add-new-project.md create mode 100644 terraform-incubator/access-the-data/main.tf create mode 100644 terraform-incubator/access-the-data/versions.tf create mode 100644 terraform-incubator/shared_resources/acm/main.tf create mode 100644 terraform-incubator/shared_resources/alb/main.tf create mode 100644 terraform-incubator/shared_resources/alb/moves.tf create mode 100644 terraform-incubator/shared_resources/rds/main.tf create mode 100644 terraform-incubator/shared_resources/rds/moves.tf create mode 100644 terraform-modules/ecs-task/ecs_autoscaling.tf create mode 100644 terraform-modules/ecs-task/main.tf create mode 100644 terraform-modules/ecs-task/variables.tf create mode 100644 terraform-modules/multi-container-service/database.tf create mode 100644 terraform-modules/multi-container-service/fargate.tf create mode 100644 terraform-modules/multi-container-service/r53.tf create mode 100644 terraform-modules/multi-container-service/tasks.tf create mode 100644 terraform-modules/multi-container-service/tls.tf create mode 100644 terraform-modules/multi-container-service/variables.tf diff --git a/documentation/terraform-add-new-project.md b/documentation/terraform-add-new-project.md new file mode 100644 index 0000000..d2b6827 --- /dev/null +++ b/documentation/terraform-add-new-project.md @@ -0,0 +1,22 @@ +# Adding a new Project to Terraform + +* Fork Incubator (if you haven't) +* Pull main branch +* Create feature branch + +```shell +> mkdir -p terraform-incubator/{projectname}/project terraform-incubator/{projectname}/dev +``` + +... + +* Commit +* Push +* Create PR to Incubator + +``` +aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 035866691871.dkr.ecr.us-west-2.amazonaws.com +``` + + +## ACM? diff --git a/terraform-incubator/access-the-data/main.tf b/terraform-incubator/access-the-data/main.tf new file mode 100644 index 0000000..44882f6 --- /dev/null +++ b/terraform-incubator/access-the-data/main.tf @@ -0,0 +1,73 @@ +locals { + // we use tf to create the zone, but other projects might + // have an existing zone and get it with a data block + zone_id = aws_route53_zone.this.zone_id + + zone_name = "accessthedata.org" + + envs = { + dev = { + environment = "dev" + db_name = "access-the-data-dev" + host_names = ["dev"] + container_env = {} + } + } +} +resource "aws_route53_zone" "this" { + name = local.zone_name +} + +module "access-the-data" { + for_each = local.envs + + source = "../../terraform-modules/multi-container-service" + + shared_configuration = local.shared_configuration + + region = "us-west-2" + project_name = "access-the-data" + application_type = "fullstack" + environment = each.value.environment + zone_id = local.zone_id + + vpc_cidr = "10.10.0.0/16" + + containers = { + ckan = { + tag = "latest" + cpu = 256 + memory = 512 + port = 80 + db_access = true + subdomains = each.value.host_names + path_patterns = ["/*"] + env_vars = merge({ + DATABASE = "postgres" + }, lookup(each.value.container_env, "ckan", {})) + } + + datapusher = { + tag = "latest" + cpu = 256 + memory = 512 + } + + solr = { + tag = "latest" + cpu = 256 + memory = 512 + } + + redis = { + tag = "latest" + cpu = 256 + memory = 512 + } + } + + postgres_database = { + db_name = each.value.db_name + username = "ckan" + } +} diff --git a/terraform-incubator/access-the-data/versions.tf b/terraform-incubator/access-the-data/versions.tf new file mode 100644 index 0000000..f25a271 --- /dev/null +++ b/terraform-incubator/access-the-data/versions.tf @@ -0,0 +1,58 @@ +// Get configuration from the shared infrastructure +data "terraform_remote_state" "shared" { + backend = "s3" + + config = { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/./terraform.tfstate" + region = "us-west-2" + } +} + +locals { + shared_configuration = data.terraform_remote_state.shared.outputs.configuration +} + +provider "aws" { + region = "us-west-2" +} + +// Set up Postgres provider to create the database +terraform { + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "~> 1.21.0" + } + } +} +data "aws_ssm_parameter" "rds_credentials" { + name = "rds_credentials" +} +data "aws_db_instance" "shared" { + db_instance_identifier = local.shared_configuration.db_identifier +} +provider "postgresql" { + host = data.aws_db_instance.shared.address + password = data.aws_ssm_parameter.rds_credentials.value + username = "postgres" + superuser = false +} + +// Create an Apex DNS record that aliases to the LB +data "aws_lb" "lb" { + arn = local.shared_configuration.alb_arn +} +resource "aws_route53_record" "apex" { + zone_id = local.zone_id + name = aws_route53_zone.this.name + type = "A" + + alias { + name = data.aws_lb.lb.dns_name + zone_id = data.aws_lb.lb.zone_id + evaluate_target_health = true + } +} diff --git a/terraform-incubator/people-depot/project/main.tf b/terraform-incubator/people-depot/project/main.tf index 561f4ac..cc3c0c9 100644 --- a/terraform-incubator/people-depot/project/main.tf +++ b/terraform-incubator/people-depot/project/main.tf @@ -16,7 +16,7 @@ module "people_depot" { container_cpu = 256 aws_managed_dns = false container_env_vars = { - SQL_HOST = "incubator-prod-database.cewewwrvdqjn.us-west-2.rds.amazonaws.com" + SQL_HOST = data.terraform_remote_state.shared.outputs.db_instance_endpoint COGNITO_USER_POOL = "us-west-2_Fn4rkZpuB" COGNITO_AWS_REGION = "us-west-2" diff --git a/terraform-incubator/shared_resources/acm/main.tf b/terraform-incubator/shared_resources/acm/main.tf new file mode 100644 index 0000000..a462ac7 --- /dev/null +++ b/terraform-incubator/shared_resources/acm/main.tf @@ -0,0 +1,25 @@ +terraform { + backend "s3" { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/acm/terraform.tfstate" + region = "us-west-2" + } +} + +provider "aws" { + region = "us-west-2" +} + +module "acm" { + source = "../../../terraform-modules/acm" + + #domain_names = ["ballotnav.org", "civictechindex.org", "vrms.io", "homeunite.us"] + domain_names = ["ballotnav.org", "civictechindex.org", "vrms.io"] + tags = { terraform_managed = "true", last_changed = formatdate("EEE YYYY-MMM-DD hh:mm:ss", timestamp()) } +} + +output "acm_certificate_arns" { + value = module.acm.acm_certificate_arns +} diff --git a/terraform-incubator/shared_resources/alb/main.tf b/terraform-incubator/shared_resources/alb/main.tf new file mode 100644 index 0000000..13f7b27 --- /dev/null +++ b/terraform-incubator/shared_resources/alb/main.tf @@ -0,0 +1,74 @@ +terraform { + backend "s3" { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/alb/terraform.tfstate" + region = "us-west-2" + } +} + +provider "aws" { + region = "us-west-2" +} + +data "terraform_remote_state" "shared" { + for_each = toset(["network", "acm"]) + backend = "s3" + + config = { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/${each.key}/terraform.tfstate" + region = "us-west-2" + } +} + +module "alb" { + source = "../../../terraform-modules/applicationlb" + + vpc_id = data.terraform_remote_state.shared["network"].outputs.vpc_id + public_subnet_ids = data.terraform_remote_state.shared["network"].outputs.public_subnet_ids + acm_certificate_arns = data.terraform_remote_state.shared["acm"].outputs.acm_certificate_arns + + // Input from Variables + environment = "prod" + region = "us-west-2" + resource_name = "incubator" + default_alb_url = "www.hackforla.org" + + tags = { terraform_managed = "true", last_changed = formatdate("EEE YYYY-MMM-DD hh:mm:ss", timestamp()) } +} + +output "alb_id" { + value = module.alb.alb_id +} + +output "security_group_id" { + value = module.alb.security_group_id +} + +output "lb_dns_name" { + value = module.alb.lb_dns_name +} + +output "lb_zone_id" { + value = module.alb.lb_zone_id +} + +output "lb_arn" { + value = module.alb.lb_arn +} + +output "alb_target_group_arn" { + value = module.alb.alb_target_group_arn +} + +output "alb_target_group_id" { + value = module.alb.alb_target_group_arn +} + +output "alb_https_listener_arn" { + value = module.alb.alb_https_listener_arn +} diff --git a/terraform-incubator/shared_resources/alb/moves.tf b/terraform-incubator/shared_resources/alb/moves.tf new file mode 100644 index 0000000..1e1571d --- /dev/null +++ b/terraform-incubator/shared_resources/alb/moves.tf @@ -0,0 +1,20 @@ +moved { + from = aws_lb.alb + to = module.alb.aws_lb.alb +} +moved { + from = aws_lb_listener.http_redirect + to = module.alb.aws_lb_listener.http_redirect +} +moved { + from = aws_lb_listener.ssl + to = module.alb.aws_lb_listener.ssl +} +moved { + from = aws_lb_listener_certificate.example["arn:aws:acm:us-west-2:035866691871:certificate/4db5d979-9797-4689-a9e9-58b7ac55c79d"] + to = module.alb.aws_lb_listener_certificate.example["arn:aws:acm:us-west-2:035866691871:certificate/4db5d979-9797-4689-a9e9-58b7ac55c79d"] +} +moved { + from = aws_security_group.alb + to = module.alb.aws_security_group.alb +} diff --git a/terraform-incubator/shared_resources/all/main.tf b/terraform-incubator/shared_resources/all/main.tf index a577001..807c3e5 100644 --- a/terraform-incubator/shared_resources/all/main.tf +++ b/terraform-incubator/shared_resources/all/main.tf @@ -26,31 +26,72 @@ data "terraform_remote_state" "shared" { } } +locals { + configuration = { + alb_arn = data.terraform_remote_state.shared["alb"].outputs.lb_arn + # XXX Prefer using the ARN to set up a data.aws_lb and get values from there + alb_external_dns = data.terraform_remote_state.shared["alb"].outputs.lb_dns_name + alb_zone_id = data.terraform_remote_state.shared["alb"].outputs.lb_zone_id + alb_security_group_id = data.terraform_remote_state.shared["alb"].outputs.security_group_id + alb_https_listener_arn = data.terraform_remote_state.shared["alb"].outputs.alb_https_listener_arn + + cluster_id = data.terraform_remote_state.shared["ecs"].outputs.cluster_id + # XXX Prefer using the cluster_id to set up a data.aws_ecs and get values from there + cluster_name = data.terraform_remote_state.shared["ecs"].outputs.cluster_name + task_execution_role_arn = data.terraform_remote_state.shared["ecs"].outputs.task_execution_role_arn + + db_identifier = data.terraform_remote_state.shared["rds"].outputs.db_identifier + # XXX Prefer using the identifier to set up a data.aws_db_instance + db_instance_endpoint = data.terraform_remote_state.shared["rds"].outputs.db_instance_endpoint + + vpc_id = data.terraform_remote_state.shared["network"].outputs.vpc_id + public_subnet_ids = data.terraform_remote_state.shared["network"].outputs.public_subnet_ids + } +} + +# XXX individual outputs are not preferred - +# instead use the configuration output +output "alb_arn" { + value = local.configuration.alb_arn +} output "alb_external_dns" { - value = data.terraform_remote_state.shared["alb"].outputs.lb_dns_name + value = local.configuration.alb_external_dns +} +output "alb_zone_id" { + value = local.configuration.alb_zone_id } output "alb_security_group_id" { - value = data.terraform_remote_state.shared["alb"].outputs.security_group_id + value = local.configuration.alb_security_group_id } output "alb_https_listener_arn" { - value = data.terraform_remote_state.shared["alb"].outputs.alb_https_listener_arn + value = local.configuration.alb_https_listener_arn } output "cluster_id" { - value = data.terraform_remote_state.shared["ecs"].outputs.cluster_id + value = local.configuration.cluster_id +} +output "cluster_name" { + value = local.configuration.cluster_name } output "task_execution_role_arn" { - value = data.terraform_remote_state.shared["ecs"].outputs.task_execution_role_arn + value = local.configuration.task_execution_role_arn +} +output "db_identifier" { + value = local.configuration.db_identifier } output "db_instance_endpoint" { - value = data.terraform_remote_state.shared["rds"].outputs.db_instance_endpoint + value = local.configuration.db_instance_endpoint } output "vpc_id" { - value = data.terraform_remote_state.shared["network"].outputs.vpc_id + value = local.configuration.vpc_id } output "public_subnet_ids" { - value = data.terraform_remote_state.shared["network"].outputs.public_subnet_ids + value = local.configuration.public_subnet_ids } #value = data.terraform_remote_state.shared["network"].outputs.vpc_cidr #value = data.terraform_remote_state.shared["multi-db"].outputs.lambda_function #value = data.terraform_remote_state.shared["ecs"].outputs.cluster_name + +output "configuration" { + value = local.configuration +} diff --git a/terraform-incubator/shared_resources/rds/main.tf b/terraform-incubator/shared_resources/rds/main.tf new file mode 100644 index 0000000..273150d --- /dev/null +++ b/terraform-incubator/shared_resources/rds/main.tf @@ -0,0 +1,89 @@ +terraform { + backend "s3" { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/rds/terraform.tfstate" + region = "us-west-2" + } +} + +provider "aws" { + region = "us-west-2" +} + +data "aws_ssm_parameter" "rds_credentials" { + name = "rds_credentials" +} + +// XXX ew +data "terraform_remote_state" "network" { + backend = "s3" + + config = { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/network/terraform.tfstate" + region = "us-west-2" + } +} + +locals { + account_vars = { + aws_region = "us-west-2" + namespace = "hfla" + resource_name = "incubator" + } + + aws_region = local.account_vars.aws_region + resource_name = local.account_vars.resource_name + env = "prod" + tags = { terraform_managed = "true", last_changed = formatdate("EEE YYYY-MMM-DD hh:mm:ss", timestamp()) } + + db_public_access = true + db_snapshot_migration = "" + db_username = "postgres" + db_password = data.aws_ssm_parameter.rds_credentials.value +} + +module "rds" { + source = "../../../terraform-modules/rds" + + resource_name = local.resource_name + environment = local.env + region = local.aws_region + + create_db_instance = true + db_public_access = local.db_public_access + db_snapshot_migration = local.db_snapshot_migration + db_username = local.db_username + db_password = local.db_password + + vpc_id = data.terraform_remote_state.network.outputs.vpc_id + vpc_cidr = data.terraform_remote_state.network.outputs.vpc_cidr + public_subnet_ids = data.terraform_remote_state.network.outputs.public_subnet_ids + public_subnet_cidrs = data.terraform_remote_state.network.outputs.public_subnet_cidrs + private_subnet_ids = data.terraform_remote_state.network.outputs.public_subnet_ids + private_subnet_cidrs = data.terraform_remote_state.network.outputs.public_subnet_cidrs +} + +output "db_identifier" { + value = module.rds.db_identifier +} + +output "db_address" { + value = module.rds.db_address +} + +output "db_instance_hosted_zone_id" { + value = module.rds.db_instance_hosted_zone_id +} + +output "db_instance_endpoint" { + value = module.rds.db_instance_endpoint +} + +output "db_security_group_id" { + value = module.rds.db_security_group_id +} diff --git a/terraform-incubator/shared_resources/rds/moves.tf b/terraform-incubator/shared_resources/rds/moves.tf new file mode 100644 index 0000000..b1510d0 --- /dev/null +++ b/terraform-incubator/shared_resources/rds/moves.tf @@ -0,0 +1,12 @@ +moved { + from = aws_db_instance.this[0] + to = module.rds.aws_db_instance.this[0] +} +moved { + from = aws_db_subnet_group.this + to = module.rds.aws_db_subnet_group.this +} +moved { + from = aws_security_group.db + to = module.rds.aws_security_group.db +} diff --git a/terraform-modules/applicationlb/outputs.tf b/terraform-modules/applicationlb/outputs.tf index b81b2a4..edf4491 100644 --- a/terraform-modules/applicationlb/outputs.tf +++ b/terraform-modules/applicationlb/outputs.tf @@ -1,3 +1,7 @@ +output "alb_id" { + value = aws_lb.alb.id +} + output "security_group_id" { value = aws_security_group.alb.id } @@ -6,6 +10,10 @@ output "lb_dns_name" { value = aws_lb.alb.dns_name } +output "lb_zone_id" { + value = aws_lb.alb.zone_id +} + output "lb_arn" { value = aws_lb.alb.arn } diff --git a/terraform-modules/ecr/main.tf b/terraform-modules/ecr/main.tf index 9bcbbd7..b7e5146 100644 --- a/terraform-modules/ecr/main.tf +++ b/terraform-modules/ecr/main.tf @@ -1,3 +1,4 @@ +// XXX Decommision - not enough here to merit separate module // -------------------------- // General Variables // -------------------------- diff --git a/terraform-modules/ecs-task/ecs_autoscaling.tf b/terraform-modules/ecs-task/ecs_autoscaling.tf new file mode 100644 index 0000000..374409a --- /dev/null +++ b/terraform-modules/ecs-task/ecs_autoscaling.tf @@ -0,0 +1,43 @@ +locals { + ecs_service = var.launch_type == "FARGATE" ? aws_ecs_service.fargate[0].name : aws_ecs_service.ec2[0].name +} + +resource "aws_appautoscaling_target" "ecs_target" { + max_capacity = 4 + min_capacity = 1 + resource_id = "service/${var.shared_configuration.cluster_name}/${local.ecs_service}" + scalable_dimension = "ecs:service:DesiredCount" + service_namespace = "ecs" +} + +resource "aws_appautoscaling_policy" "ecs_autoscale_memory" { + name = "ecs_autoscale_memory" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.ecs_target.resource_id + scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension + service_namespace = aws_appautoscaling_target.ecs_target.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageMemoryUtilization" + } + + target_value = 80 + } +} + +resource "aws_appautoscaling_policy" "ecs_autoscale_cpu" { + name = "ecs_autoscale_cpu" + policy_type = "TargetTrackingScaling" + resource_id = aws_appautoscaling_target.ecs_target.resource_id + scalable_dimension = aws_appautoscaling_target.ecs_target.scalable_dimension + service_namespace = aws_appautoscaling_target.ecs_target.service_namespace + + target_tracking_scaling_policy_configuration { + predefined_metric_specification { + predefined_metric_type = "ECSServiceAverageCPUUtilization" + } + + target_value = 60 + } +} diff --git a/terraform-modules/ecs-task/main.tf b/terraform-modules/ecs-task/main.tf new file mode 100644 index 0000000..ea115cd --- /dev/null +++ b/terraform-modules/ecs-task/main.tf @@ -0,0 +1,179 @@ +locals { + envappname = "${var.project_name}-${var.application_type}-${var.environment}" + + # Conditionals for container compute resources + # 0 CPU means unlimited cpu access + # 0 memory is invalid, thus it defaults to 128mb + container_cpu = var.launch_type == "FARGATE" ? var.container_cpu : var.container_cpu == 0 ? 128 : var.container_cpu + container_memory = var.launch_type == "FARGATE" ? var.container_memory : var.container_memory == 0 ? 256 : var.container_memory + + task_cpu = var.launch_type == "FARGATE" ? var.container_cpu : var.container_cpu == 0 ? null : var.container_cpu + task_memory = var.launch_type == "FARGATE" ? var.container_memory : var.container_memory == 0 ? 128 : var.container_memory + + task_network_mode = var.launch_type == "FARGATE" ? "awsvpc" : "bridge" + host_port = var.launch_type == "FARGATE" ? var.container_port : 0 + target_type = var.launch_type == "FARGATE" ? "ip" : "instance" + + public_address = length(var.host_names) > 0 +} + +module "application_container_def" { + source = "cloudposse/ecs-container-definition/aws" + version = "0.56.0" + + container_name = local.envappname + container_image = var.container_image + container_cpu = var.container_cpu + container_memory_reservation = local.container_memory + port_mappings = [ + { + containerPort = var.container_port + hostPort = local.host_port + protocol = "tcp" + } + ] + map_environment = var.container_env_vars + map_secrets = var.container_secrets + + log_configuration = { + logDriver = "awslogs" + options = { + awslogs-group = format("ecs/%s", local.envappname) // XXX + awslogs-region = var.region + awslogs-stream-prefix = var.application_type + } + } + linux_parameters = { + initProcessEnabled = true + capabilities = null + devices = null + maxSwap = null + sharedMemorySize = null + swappiness = null + tmpfs = null + } +} + +resource "aws_ecs_task_definition" "task" { + family = local.envappname + + container_definitions = jsonencode([ + module.application_container_def.json_map_object + ]) + + requires_compatibilities = [var.launch_type] + network_mode = local.task_network_mode + task_role_arn = var.shared_configuration.task_execution_role_arn + execution_role_arn = var.shared_configuration.task_execution_role_arn + memory = local.task_memory + cpu = local.task_cpu +} + + +resource "aws_lb_target_group" "this" { + count = local.public_address ? 1 : 0 + + target_type = local.target_type + name = local.envappname + port = 80 + protocol = "HTTP" + vpc_id = var.shared_configuration.vpc_id + deregistration_delay = 5 + stickiness { + type = "lb_cookie" + } + health_check { + path = var.health_check_path + interval = 15 + healthy_threshold = 3 + unhealthy_threshold = 2 + matcher = "200,302" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_lb_listener_rule" "static" { + count = local.public_address ? 1 : 0 + + listener_arn = var.shared_configuration.alb_https_listener_arn + + action { + type = "forward" + target_group_arn = aws_lb_target_group.this[0].arn + } + + condition { + host_header { + values = var.host_names + } + } + + # Path Pattern condition + dynamic "condition" { + for_each = length(var.path_patterns) == 0 ? [] : [var.path_patterns] + + content { + path_pattern { + values = var.path_patterns + } + } + } +} +resource "aws_ecs_service" "ec2" { + count = var.launch_type == "FARGATE" ? 0 : 1 + name = local.envappname + cluster = var.shared_configuration.cluster_id + enable_execute_command = true + task_definition = aws_ecs_task_definition.task.arn + launch_type = var.launch_type + desired_count = var.desired_count + + dynamic "load_balancer" { + for_each = local.public_address ? [1] : [] + content { + container_name = local.envappname + container_port = var.container_port + target_group_arn = aws_lb_target_group.this[0].arn + } + } + + depends_on = [aws_lb_listener_rule.static] // XXX put into module refs + + lifecycle { + ignore_changes = [task_definition, desired_count] + } +} + +resource "aws_ecs_service" "fargate" { + count = var.launch_type == "FARGATE" ? 1 : 0 + name = local.envappname + cluster = var.shared_configuration.cluster_id + enable_execute_command = true + task_definition = aws_ecs_task_definition.task.arn + launch_type = var.launch_type + desired_count = var.desired_count + + network_configuration { + subnets = var.shared_configuration.public_subnet_ids + security_groups = [var.fargate_security_group_id] + assign_public_ip = true + } + + dynamic "load_balancer" { + for_each = local.public_address ? [1] : [] + content { + container_name = local.envappname + container_port = var.container_port + target_group_arn = aws_lb_target_group.this[0].arn + } + } + + depends_on = [aws_lb_listener_rule.static] + + lifecycle { + ignore_changes = [desired_count] + } +} diff --git a/terraform-modules/ecs-task/variables.tf b/terraform-modules/ecs-task/variables.tf new file mode 100644 index 0000000..0c87327 --- /dev/null +++ b/terraform-modules/ecs-task/variables.tf @@ -0,0 +1,84 @@ +variable "shared_configuration" { + description = "Configuration object from shared resources" + type = object({ + alb_https_listener_arn = string + cluster_id = string + cluster_name = string + task_execution_role_arn = string + vpc_id = string + public_subnet_ids = set(string) + }) +} + +variable "project_name" { + description = "The overall name of the project using this infrastructure; used to group related resources by" +} + +variable "environment" { + type = string +} + +variable "region" { + type = string +} + +variable "launch_type" { + default = "FARGATE" + type = string + description = "How to launch the container within ECS EC2 instance or FARGATE" +} + +variable "application_type" { + type = string + description = "defines what type of application is running, fullstack, client, backend, etc. will be used for cloudwatch logs" +} + +variable "fargate_security_group_id" { + type = string +} + +variable "container_image" { + type = string +} + +variable "container_cpu" { + type = number + default = 0 +} + +variable "container_memory" { + type = number + default = 0 +} + +variable "container_port" { + type = number + default = 80 +} + +variable "container_env_vars" { + type = map(any) +} + +variable "container_secrets" { + type = map(any) +} + +variable "desired_count" { + default = 1 + type = number +} + +variable "host_names" { + type = list(string) + default = [] +} + +variable "path_patterns" { + type = list(string) + default = [] +} + +variable "health_check_path" { + type = string +} diff --git a/terraform-modules/multi-container-service/database.tf b/terraform-modules/multi-container-service/database.tf new file mode 100644 index 0000000..fda7303 --- /dev/null +++ b/terraform-modules/multi-container-service/database.tf @@ -0,0 +1,42 @@ +terraform { + required_providers { + postgresql = { + source = "cyrilgdn/postgresql" + version = "~> 1.21.0" + } + } +} + +data "aws_secretsmanager_random_password" "db_password_init" { + password_length = 48 +} + +data "aws_db_instance" "shared" { + db_instance_identifier = var.shared_configuration.db_identifier +} + +// We're using the random_password data source to initialize this; +// we use the lifecycle.ignore_changes to say that we don't want +// the value to be updated. We get most of the benefit of a +// Secret Manager entry, and save 0.40 USD/mo +resource "aws_ssm_parameter" "rds_dbowner_password" { + name = "rds_password_${var.postgres_database.db_name}_${var.environment}" + type = "String" + value = data.aws_secretsmanager_random_password.db_password_init.random_password + lifecycle { + ignore_changes = [value] + } +} + +resource "postgresql_role" "db_owner" { + count = var.postgres_database.username != "" ? 1 : 0 + name = "${var.postgres_database.username}_${var.environment}" + login = true + password = aws_ssm_parameter.rds_dbowner_password.value +} + +resource "postgresql_database" "db" { + count = var.postgres_database.db_name != "" ? 1 : 0 + name = "${var.postgres_database.db_name}_${var.environment}" + owner = postgresql_role.db_owner[0].name +} diff --git a/terraform-modules/multi-container-service/fargate.tf b/terraform-modules/multi-container-service/fargate.tf new file mode 100644 index 0000000..b23d905 --- /dev/null +++ b/terraform-modules/multi-container-service/fargate.tf @@ -0,0 +1,22 @@ +resource "aws_security_group" "fargate" { + name = "ecs_fargate_${local.envappname}" + description = "Allow TLS inbound traffic" + vpc_id = var.shared_configuration.vpc_id + + ingress { + description = "All Internal traffic" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { Name = "ecs_container_instance_${local.envappname}" } +} diff --git a/terraform-modules/multi-container-service/r53.tf b/terraform-modules/multi-container-service/r53.tf new file mode 100644 index 0000000..7d49aac --- /dev/null +++ b/terraform-modules/multi-container-service/r53.tf @@ -0,0 +1,13 @@ +data "aws_lb" "lb" { + arn = var.shared_configuration.alb_arn +} + +resource "aws_route53_record" "cname" { + for_each = toset([for n in flatten([for c in var.containers : c.subdomains]) : n]) + + zone_id = var.zone_id + name = each.value + type = "CNAME" + ttl = "300" + records = [data.aws_lb.lb.dns_name] +} diff --git a/terraform-modules/multi-container-service/tasks.tf b/terraform-modules/multi-container-service/tasks.tf new file mode 100644 index 0000000..3a4223e --- /dev/null +++ b/terraform-modules/multi-container-service/tasks.tf @@ -0,0 +1,53 @@ +resource "aws_ecr_repository" "these" { + for_each = var.containers + name = "${var.project_name}-${var.application_type}-${each.key}" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } +} + +// Create group for streaming application logs +resource "aws_cloudwatch_log_group" "cwlogs" { + name = "ecs/${local.envappname}" + retention_in_days = 14 +} + + +data "aws_route53_zone" "this" { + zone_id = var.zone_id +} + +module "ecs-task" { + for_each = var.containers + + source = "../ecs-task" + shared_configuration = var.shared_configuration + + fargate_security_group_id = aws_security_group.fargate.id + project_name = var.project_name + environment = var.environment + region = var.region + application_type = each.key + + container_image = "${aws_ecr_repository.these[each.key].repository_url}:${each.value.tag}" + launch_type = each.value.launch_type + desired_count = each.value.desired_count + container_cpu = each.value.cpu + container_memory = each.value.memory + + host_names = [for s in each.value.subdomains : "${s}.${data.aws_route53_zone.this.name}"] + path_patterns = each.value.path_patterns + health_check_path = each.value.health_check_path + container_port = each.value.port + container_env_vars = each.value.db_access ? merge({ + SQL_USER = postgresql_role.db_owner[0].name + SQL_DATABASE = postgresql_database.db[0].name + SQL_HOST = data.aws_db_instance.shared.address + SQL_PORT = 5432 + }, each.value.env_vars) : each.value.env_vars + container_secrets = each.value.db_access ? { + SQL_PASSWORD = aws_ssm_parameter.rds_dbowner_password.arn + } : {} +} diff --git a/terraform-modules/multi-container-service/tls.tf b/terraform-modules/multi-container-service/tls.tf new file mode 100644 index 0000000..f34f966 --- /dev/null +++ b/terraform-modules/multi-container-service/tls.tf @@ -0,0 +1,8 @@ +resource "aws_acm_certificate" "cert" { + domain_name = "example.com" + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} diff --git a/terraform-modules/multi-container-service/variables.tf b/terraform-modules/multi-container-service/variables.tf new file mode 100644 index 0000000..472cc22 --- /dev/null +++ b/terraform-modules/multi-container-service/variables.tf @@ -0,0 +1,86 @@ +locals { + envname = "${var.project_name}-${var.environment}" + envappname = "${var.project_name}-${var.application_type}-${var.environment}" +} + +# c.f. terraform-incubator shared_configuration/all +# XXX We'd like to reduce this to ARNs and IDs and just use `data` blocks +variable "shared_configuration" { + description = "Configuration object from shared resources" + type = object({ + alb_arn = string + alb_https_listener_arn = string + cluster_id = string + cluster_name = string + task_execution_role_arn = string + db_identifier = string + vpc_id = string + public_subnet_ids = set(string) + }) +} + +variable "region" { + type = string +} + +variable "project_name" { + description = "The overall name of the project using this infrastructure; used to group related resources" +} + +variable "application_type" { + type = string + description = "defines what type of application is running, fullstack, client, backend, etc. will be used for cloudwatch logs" +} + +variable "environment" { + type = string +} + +variable "zone_id" { + type = string + description = "The root zone_id for the service" +} + +variable "vpc_cidr" { + type = string + description = "VPC cidr block" +} + +variable "tags" { + default = { terraform_managed = "true" } + type = map(any) +} + +// -------------------------- +// Container Definition Variables +// -------------------------- + +variable "containers" { + type = map(object({ + tag = optional(string, "latest") + desired_count = optional(number, 1) + launch_type = optional(string, "FARGATE") + cpu = optional(number, 0) + memory = optional(number, 0) + port = optional(number, 80) + db_access = optional(bool, false) + subdomains = optional(list(string), []) + path_patterns = optional(list(string), []) + health_check_path = optional(string, "/") + env_vars = optional(map(any), {}) + })) + + description = "Per container service configuration. Note that subdomains are used (e.g. 'www' not 'www.example.com')" +} + +variable "postgres_database" { + type = object({ + db_name = string + username = string + }) + default = { + db_name = "" + username = "" + } + description = "non-empty map will create a database and users for application" +} diff --git a/terraform-modules/rds/main.tf b/terraform-modules/rds/main.tf index 4a5c426..0e1af55 100644 --- a/terraform-modules/rds/main.tf +++ b/terraform-modules/rds/main.tf @@ -1,6 +1,7 @@ locals { db_subnet_ids = var.db_public_access ? var.public_subnet_ids : var.private_subnet_ids } + resource "aws_db_instance" "this" { count = var.create_db_instance ? 1 : 0 diff --git a/terraform-modules/rds/outputs.tf b/terraform-modules/rds/outputs.tf index e48c6d1..b4d9f71 100644 --- a/terraform-modules/rds/outputs.tf +++ b/terraform-modules/rds/outputs.tf @@ -1,3 +1,8 @@ +output "db_identifier" { + description = "The AWS provided identifier" + value = aws_db_instance.this[0].identifier +} + output "db_address" { description = "The aws provided URL of the database" value = element(concat(aws_db_instance.this.*.address, [""]), 0) diff --git a/terraform-modules/service/alb_resources.tf b/terraform-modules/service/alb_resources.tf index 86ea95b..96bf520 100644 --- a/terraform-modules/service/alb_resources.tf +++ b/terraform-modules/service/alb_resources.tf @@ -48,6 +48,4 @@ resource "aws_lb_listener_rule" "static" { } } } - - depends_on = [aws_lb_target_group.this] } diff --git a/terraform-modules/service/ecs_ec2.tf b/terraform-modules/service/ecs_ec2.tf index f76a9f8..8ad2e6e 100644 --- a/terraform-modules/service/ecs_ec2.tf +++ b/terraform-modules/service/ecs_ec2.tf @@ -1,12 +1,11 @@ - resource "aws_ecs_service" "ec2" { - count = var.launch_type == "FARGATE" ? 0 : 1 - name = local.envappname - cluster = var.cluster_id + count = var.launch_type == "FARGATE" ? 0 : 1 + name = local.envappname + cluster = var.cluster_id enable_execute_command = true - task_definition = aws_ecs_task_definition.task.arn - launch_type = var.launch_type - desired_count = var.desired_count + task_definition = aws_ecs_task_definition.task.arn + launch_type = var.launch_type + desired_count = var.desired_count load_balancer { container_name = local.envappname diff --git a/terraform-modules/service/task_definition.tf b/terraform-modules/service/task_definition.tf index e2bf925..d85a2be 100644 --- a/terraform-modules/service/task_definition.tf +++ b/terraform-modules/service/task_definition.tf @@ -21,12 +21,12 @@ locals { } module "application_container_def" { - source = "cloudposse/ecs-container-definition/aws" - version = "0.56.0" + source = "cloudposse/ecs-container-definition/aws" + version = "0.56.0" - container_name = local.envappname - container_image = var.container_image - container_cpu = var.container_cpu + container_name = local.envappname + container_image = var.container_image + container_cpu = var.container_cpu container_memory_reservation = local.container_memory port_mappings = [ { @@ -45,13 +45,13 @@ module "application_container_def" { } } linux_parameters = { - initProcessEnabled = true - capabilities = null - devices = null - maxSwap = null - sharedMemorySize = null - swappiness = null - tmpfs = null + initProcessEnabled = true + capabilities = null + devices = null + maxSwap = null + sharedMemorySize = null + swappiness = null + tmpfs = null } } @@ -64,7 +64,7 @@ resource "aws_ecs_task_definition" "task" { requires_compatibilities = [var.launch_type] network_mode = local.task_network_mode - task_role_arn = var.task_execution_role_arn + task_role_arn = var.task_execution_role_arn execution_role_arn = var.task_execution_role_arn memory = local.task_memory cpu = local.task_cpu From 9b11be6cd3fbf3644f86ec69be4f0c58800376e4 Mon Sep 17 00:00:00 2001 From: Judson Lester Date: Sat, 4 Nov 2023 00:14:52 -0700 Subject: [PATCH 2/4] ATD setting up env vars --- terraform-incubator/access-the-data/main.tf | 97 +++++++++++++++---- terraform-incubator/access-the-data/moves.tf | 8 ++ .../access-the-data/versions.tf | 16 --- .../shared_resources/ecs/main.tf | 68 +++++++++++++ terraform-modules/cheap-secrets/main.tf | 31 ++++++ .../database.tf => database/main.tf} | 10 +- terraform-modules/database/variables.tf | 45 +++++++++ terraform-modules/ecs-task/main.tf | 6 +- terraform-modules/ecs-task/variables.tf | 19 ++-- .../multi-container-service/tasks.tf | 63 +++++++++--- .../multi-container-service/tls.tf | 8 -- .../multi-container-service/variables.tf | 13 --- terraform-modules/project-zone/main.tf | 75 ++++++++++++++ 13 files changed, 375 insertions(+), 84 deletions(-) create mode 100644 terraform-incubator/access-the-data/moves.tf create mode 100644 terraform-incubator/shared_resources/ecs/main.tf create mode 100644 terraform-modules/cheap-secrets/main.tf rename terraform-modules/{multi-container-service/database.tf => database/main.tf} (75%) create mode 100644 terraform-modules/database/variables.tf delete mode 100644 terraform-modules/multi-container-service/tls.tf create mode 100644 terraform-modules/project-zone/main.tf diff --git a/terraform-incubator/access-the-data/main.tf b/terraform-incubator/access-the-data/main.tf index 44882f6..f023143 100644 --- a/terraform-incubator/access-the-data/main.tf +++ b/terraform-incubator/access-the-data/main.tf @@ -1,21 +1,42 @@ locals { // we use tf to create the zone, but other projects might // have an existing zone and get it with a data block - zone_id = aws_route53_zone.this.zone_id - - zone_name = "accessthedata.org" + zone_id = module.zone.zone_id envs = { dev = { - environment = "dev" - db_name = "access-the-data-dev" - host_names = ["dev"] - container_env = {} + environment = "dev" + db_name = "access-the-data-dev" + host_names = ["dev"] + container_env = { + CKAN_SITE_URL = "https://dev.accessthedata.org" + } } } } -resource "aws_route53_zone" "this" { - name = local.zone_name + +module "zone" { + source = "../../terraform-modules/project-zone" + + zone_name = "accessthedata.org" + shared_configuration = local.shared_configuration +} + +module "database" { + for_each = local.envs + + source = "../../terraform-modules/database" + + environment = each.value.environment + db_name = each.value.db_name + username = "ckan" +} + +module "secrets" { + for_each = local.envs + source = "../../terraform-modules/cheap-secrets" + scope-name = "ckan-${each.value}" + secret-names = ["csrf", "admin-password"] } module "access-the-data" { @@ -35,16 +56,57 @@ module "access-the-data" { containers = { ckan = { - tag = "latest" - cpu = 256 - memory = 512 - port = 80 - db_access = true + tag = "latest" + cpu = 256 + memory = 512 + port = 80 + subdomains = each.value.host_names path_patterns = ["/*"] env_vars = merge({ - DATABASE = "postgres" + DATABASE = "postgres" + POSTGRES_HOST = module.database.host + POSTGRES_PORT = module.database.port + + // SQLALCHEMY has been set up in the container = + // we don't know the PG password, so we can't build the URLs + + # Taken verbatim from .env + CKAN_DB = module.database.database + CKAN_DB_USER = module.database.user + CKAN_VERSION = "2.10.0" + CKAN_SITE_ID = "default" + + CKAN_PORT = "5000" + CKAN_PORT_HOST = "5000" + + CKAN_SYSADMIN_NAME = "ckan_admin" + CKAN_SYSADMIN_EMAIL = "your_email@example.com" + CKAN_STORAGE_PATH = "/var/lib/ckan" + + CKAN_SMTP_SERVER = "smtp.hackforla.org:25" + CKAN_SMTP_STARTTLS = "True" + CKAN_SMTP_USER = "user" + CKAN_SMTP_PASSWORD = "pass" + CKAN_SMTP_MAIL_FROM = "ckan@localhost" + + CKAN_SOLR_URL = "http://solr:8983/solr/ckan" + CKAN_REDIS_URL = "redis://redis:6379/1" + CKAN_DATAPUSHER_URL = "http://datapusher:8800" + CKAN__DATAPUSHER__CALLBACK_URL_BASE = "http://ckan:5000" + CKAN__HARVEST__MQ__HOSTNAME = "redis" + + CKAN__PLUGINS = "envvars image_view text_view recline_view datastore datapusher ckanext_hack4laatd" + CKAN__HARVEST__MQ__TYPE = "redis" + CKAN__HARVEST__MQ__PORT = "6379" + CKAN__HARVEST__MQ__REDIS_DB = "1" + CKAN__FAVICON = "favicon.png" }, lookup(each.value.container_env, "ckan", {})) + secrets = { + CKAN_DB_PASSWORD = module.databse.password_arn + CKAN___BEAKER__SESSION__SECRET = module.secrets["ckan-${each.value}"].arn["csrf"] + CKAN_SYSADMIN_PASSWORD = module.secrets["ckan-${each.value}"].arn["admin-password"] + } } datapusher = { @@ -65,9 +127,4 @@ module "access-the-data" { memory = 512 } } - - postgres_database = { - db_name = each.value.db_name - username = "ckan" - } } diff --git a/terraform-incubator/access-the-data/moves.tf b/terraform-incubator/access-the-data/moves.tf new file mode 100644 index 0000000..34efa97 --- /dev/null +++ b/terraform-incubator/access-the-data/moves.tf @@ -0,0 +1,8 @@ +moved { + from = aws_route53_record.apex + to = module.zone.aws_route53_record.apex +} +moved { + from = aws_route53_zone.this + to = module.zone.aws_route53_zone.this +} diff --git a/terraform-incubator/access-the-data/versions.tf b/terraform-incubator/access-the-data/versions.tf index f25a271..5a61936 100644 --- a/terraform-incubator/access-the-data/versions.tf +++ b/terraform-incubator/access-the-data/versions.tf @@ -40,19 +40,3 @@ provider "postgresql" { username = "postgres" superuser = false } - -// Create an Apex DNS record that aliases to the LB -data "aws_lb" "lb" { - arn = local.shared_configuration.alb_arn -} -resource "aws_route53_record" "apex" { - zone_id = local.zone_id - name = aws_route53_zone.this.name - type = "A" - - alias { - name = data.aws_lb.lb.dns_name - zone_id = data.aws_lb.lb.zone_id - evaluate_target_health = true - } -} diff --git a/terraform-incubator/shared_resources/ecs/main.tf b/terraform-incubator/shared_resources/ecs/main.tf new file mode 100644 index 0000000..0d2c879 --- /dev/null +++ b/terraform-incubator/shared_resources/ecs/main.tf @@ -0,0 +1,68 @@ +terraform { + backend "s3" { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/ecs/terraform.tfstate" + region = "us-west-2" + } +} + +provider "aws" { + region = "us-west-2" +} + +// XXX ew +data "terraform_remote_state" "network" { + backend = "s3" + + config = { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/network/terraform.tfstate" + region = "us-west-2" + } +} +// XXX ew +data "terraform_remote_state" "alb" { + backend = "s3" + + config = { + bucket = "hlfa-incubator-terragrunt" + dynamodb_table = "terraform-locks" + encrypt = true + key = "terragrunt-states/incubator/alb/terraform.tfstate" + region = "us-west-2" + } +} + +locals { + account_vars = { + aws_region = "us-west-2" + namespace = "hfla" + resource_name = "incubator" + } + + aws_region = local.account_vars.aws_region + resource_name = local.account_vars.resource_name + env = "prod" + tags = { terraform_managed = "true", last_changed = formatdate("EEE YYYY-MMM-DD hh:mm:ss", timestamp()) } +} + +module "ecs" { + source = "../../../terraform-modules/ecs" + + vpc_id = data.terraform_remote_state.network.outputs.vpc_id + vpc_cidr = data.terraform_remote_state.network.outputs.vpc_cidr + public_subnet_ids = data.terraform_remote_state.network.outputs.public_subnet_ids + alb_security_group_id = data.terraform_remote_state.alb.outputs.security_group_id + + // Input from Variables + ecs_ec2_instance_count = local.ecs_ec2_instance_count + ecs_ec2_instance_type = local.ecs_ec2_instance_type + key_name = local.key_name + environment = local.env + resource_name = local.resource_name + tags = local.tags +} diff --git a/terraform-modules/cheap-secrets/main.tf b/terraform-modules/cheap-secrets/main.tf new file mode 100644 index 0000000..30dc2a2 --- /dev/null +++ b/terraform-modules/cheap-secrets/main.tf @@ -0,0 +1,31 @@ +variable "secret-names" { + type = list(string) +} + +variable "length" { + type = number + default = 48 +} + +data "aws_secretsmanager_random_password" "them" { + for_each = var.secret_names + password_length = var.length +} + +// We're using the random_password data source to initialize this; +// we use the lifecycle.ignore_changes to say that we don't want +// the value to be updated. We get most of the benefit of a +// Secret Manager entry, and save 0.40 USD/mo +resource "aws_ssm_parameter" "these" { + for_each = var.secret_names + name = each.value + type = "SecureString" + value = data.aws_secretsmanager_random_password.them[each.value].random_password + lifecycle { + ignore_changes = [value] + } +} + +output "arn" { + value = { for k, v in aws_ssm_parameter.these : k => v.arn } +} diff --git a/terraform-modules/multi-container-service/database.tf b/terraform-modules/database/main.tf similarity index 75% rename from terraform-modules/multi-container-service/database.tf rename to terraform-modules/database/main.tf index fda7303..1698adf 100644 --- a/terraform-modules/multi-container-service/database.tf +++ b/terraform-modules/database/main.tf @@ -20,7 +20,7 @@ data "aws_db_instance" "shared" { // the value to be updated. We get most of the benefit of a // Secret Manager entry, and save 0.40 USD/mo resource "aws_ssm_parameter" "rds_dbowner_password" { - name = "rds_password_${var.postgres_database.db_name}_${var.environment}" + name = "rds_password_${var.db_name}_${var.environment}" type = "String" value = data.aws_secretsmanager_random_password.db_password_init.random_password lifecycle { @@ -29,14 +29,14 @@ resource "aws_ssm_parameter" "rds_dbowner_password" { } resource "postgresql_role" "db_owner" { - count = var.postgres_database.username != "" ? 1 : 0 - name = "${var.postgres_database.username}_${var.environment}" + count = var.username != "" ? 1 : 0 + name = "${var.username}_${var.environment}" login = true password = aws_ssm_parameter.rds_dbowner_password.value } resource "postgresql_database" "db" { - count = var.postgres_database.db_name != "" ? 1 : 0 - name = "${var.postgres_database.db_name}_${var.environment}" + count = var.db_name != "" ? 1 : 0 + name = "${var.db_name}_${var.environment}" owner = postgresql_role.db_owner[0].name } diff --git a/terraform-modules/database/variables.tf b/terraform-modules/database/variables.tf new file mode 100644 index 0000000..f753f0a --- /dev/null +++ b/terraform-modules/database/variables.tf @@ -0,0 +1,45 @@ +variable "shared_configuration" { + description = "Configuration object from shared resources" + type = object({ + alb_arn = string + alb_https_listener_arn = string + cluster_id = string + cluster_name = string + task_execution_role_arn = string + db_identifier = string + vpc_id = string + public_subnet_ids = set(string) + }) +} + +variable "environment" { + type = string +} + +variable "db_name" { + type = string +} + +variable "username" { + type = string +} + +output "host" { + value = data.aws_db_instance.shared.address +} + +output "port" { + value = 5432 +} + +output "database" { + value = postgresql_database.db[0].name +} + +output "user" { + value = postgresql_role.db_owner[0].name +} + +output "password_arn" { + value = aws_ssm_parameter.rds_dbowner_password.arn +} diff --git a/terraform-modules/ecs-task/main.tf b/terraform-modules/ecs-task/main.tf index ea115cd..a51e24d 100644 --- a/terraform-modules/ecs-task/main.tf +++ b/terraform-modules/ecs-task/main.tf @@ -38,7 +38,7 @@ module "application_container_def" { log_configuration = { logDriver = "awslogs" options = { - awslogs-group = format("ecs/%s", local.envappname) // XXX + awslogs-group = var.log_group awslogs-region = var.region awslogs-stream-prefix = var.application_type } @@ -63,8 +63,8 @@ resource "aws_ecs_task_definition" "task" { requires_compatibilities = [var.launch_type] network_mode = local.task_network_mode - task_role_arn = var.shared_configuration.task_execution_role_arn - execution_role_arn = var.shared_configuration.task_execution_role_arn + task_role_arn = var.task_role_arn + execution_role_arn = var.task_role_arn memory = local.task_memory cpu = local.task_cpu } diff --git a/terraform-modules/ecs-task/variables.tf b/terraform-modules/ecs-task/variables.tf index 0c87327..e70e803 100644 --- a/terraform-modules/ecs-task/variables.tf +++ b/terraform-modules/ecs-task/variables.tf @@ -1,12 +1,11 @@ variable "shared_configuration" { description = "Configuration object from shared resources" type = object({ - alb_https_listener_arn = string - cluster_id = string - cluster_name = string - task_execution_role_arn = string - vpc_id = string - public_subnet_ids = set(string) + alb_https_listener_arn = string + cluster_id = string + cluster_name = string + vpc_id = string + public_subnet_ids = set(string) }) } @@ -33,6 +32,10 @@ variable "application_type" { description = "defines what type of application is running, fullstack, client, backend, etc. will be used for cloudwatch logs" } +variable "task_role_arn" { + type = string +} + variable "fargate_security_group_id" { type = string } @@ -82,3 +85,7 @@ variable "path_patterns" { variable "health_check_path" { type = string } + +variable "log_group" { + type = string +} diff --git a/terraform-modules/multi-container-service/tasks.tf b/terraform-modules/multi-container-service/tasks.tf index 3a4223e..299f403 100644 --- a/terraform-modules/multi-container-service/tasks.tf +++ b/terraform-modules/multi-container-service/tasks.tf @@ -19,12 +19,55 @@ data "aws_route53_zone" "this" { zone_id = var.zone_id } + +resource "aws_iam_role" "ecs_task_execution_role" { + name = "${local.envname}-ecs-task-role" + description = "Allow ECS tasks to access AWS resources" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + }, + ] + }) + + inline_policy { + name = "ecs-executor-policy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "ssm:GetParameters" + Effect = "Allow" + Resource = [aws_ssm_parameter.rds_dbowner_password.arn] + } + ] + }) + } + + tags = var.tags +} + +resource "aws_iam_role_policy_attachment" "ecs_task" { + role = aws_iam_role.ecs_task_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + + module "ecs-task" { for_each = var.containers source = "../ecs-task" shared_configuration = var.shared_configuration + task_role_arn = aws_iam_role.ecs_task_execution_role.arn fargate_security_group_id = aws_security_group.fargate.id project_name = var.project_name environment = var.environment @@ -37,17 +80,11 @@ module "ecs-task" { container_cpu = each.value.cpu container_memory = each.value.memory - host_names = [for s in each.value.subdomains : "${s}.${data.aws_route53_zone.this.name}"] - path_patterns = each.value.path_patterns - health_check_path = each.value.health_check_path - container_port = each.value.port - container_env_vars = each.value.db_access ? merge({ - SQL_USER = postgresql_role.db_owner[0].name - SQL_DATABASE = postgresql_database.db[0].name - SQL_HOST = data.aws_db_instance.shared.address - SQL_PORT = 5432 - }, each.value.env_vars) : each.value.env_vars - container_secrets = each.value.db_access ? { - SQL_PASSWORD = aws_ssm_parameter.rds_dbowner_password.arn - } : {} + host_names = [for s in each.value.subdomains : "${s}.${data.aws_route53_zone.this.name}"] + path_patterns = each.value.path_patterns + health_check_path = each.value.health_check_path + log_group = aws_cloudwatch_log_group.cwlogs.name + container_port = each.value.port + container_env_vars = each.value.env_vars + container_secrets = each.value.secrets } diff --git a/terraform-modules/multi-container-service/tls.tf b/terraform-modules/multi-container-service/tls.tf deleted file mode 100644 index f34f966..0000000 --- a/terraform-modules/multi-container-service/tls.tf +++ /dev/null @@ -1,8 +0,0 @@ -resource "aws_acm_certificate" "cert" { - domain_name = "example.com" - validation_method = "DNS" - - lifecycle { - create_before_destroy = true - } -} diff --git a/terraform-modules/multi-container-service/variables.tf b/terraform-modules/multi-container-service/variables.tf index 472cc22..ce15cbc 100644 --- a/terraform-modules/multi-container-service/variables.tf +++ b/terraform-modules/multi-container-service/variables.tf @@ -63,7 +63,6 @@ variable "containers" { cpu = optional(number, 0) memory = optional(number, 0) port = optional(number, 80) - db_access = optional(bool, false) subdomains = optional(list(string), []) path_patterns = optional(list(string), []) health_check_path = optional(string, "/") @@ -72,15 +71,3 @@ variable "containers" { description = "Per container service configuration. Note that subdomains are used (e.g. 'www' not 'www.example.com')" } - -variable "postgres_database" { - type = object({ - db_name = string - username = string - }) - default = { - db_name = "" - username = "" - } - description = "non-empty map will create a database and users for application" -} diff --git a/terraform-modules/project-zone/main.tf b/terraform-modules/project-zone/main.tf new file mode 100644 index 0000000..5b4b025 --- /dev/null +++ b/terraform-modules/project-zone/main.tf @@ -0,0 +1,75 @@ +variable "zone_name" { + type = string +} + +variable "shared_configuration" { + description = "Configuration object from shared resources" + type = object({ + alb_arn = string + alb_https_listener_arn = string + }) +} + +output "zone_id" { + value = aws_route53_zone.this.zone_id +} + +resource "aws_route53_zone" "this" { + name = var.zone_name +} + +// Create an Apex DNS record that aliases to the LB +data "aws_lb" "lb" { + arn = var.shared_configuration.alb_arn +} + +resource "aws_route53_record" "apex" { + zone_id = aws_route53_zone.this.zone_id + name = aws_route53_zone.this.name + type = "A" + + alias { + name = data.aws_lb.lb.dns_name + zone_id = data.aws_lb.lb.zone_id + evaluate_target_health = true + } +} + + +resource "aws_acm_certificate" "domain" { + domain_name = var.zone_name + validation_method = "DNS" + subject_alternative_names = ["*.${var.zone_name}"] + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.domain.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = aws_route53_zone.this.zone_id +} + +resource "aws_acm_certificate_validation" "domain" { + certificate_arn = aws_acm_certificate.domain.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} + +resource "aws_lb_listener_certificate" "domain" { + listener_arn = var.shared_configuration.alb_https_listener_arn + + certificate_arn = aws_acm_certificate_validation.domain.certificate_arn +} From 75c5a97fcd0a5c137aba2341dd3a194d24d5bb0d Mon Sep 17 00:00:00 2001 From: Judson Lester Date: Sun, 26 Nov 2023 13:15:54 -0800 Subject: [PATCH 3/4] Add service discovery --- flake.nix | 1 + terraform-incubator/access-the-data/main.tf | 23 +++++++++--------- terraform-modules/cheap-secrets/main.tf | 10 +++++--- terraform-modules/database/main.tf | 7 +++--- terraform-modules/ecs-task/main.tf | 10 +++++++- .../multi-container-service/discovery.tf | 24 +++++++++++++++++++ .../multi-container-service/tasks.tf | 24 ++++++++++++------- .../multi-container-service/variables.tf | 6 +++-- 8 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 terraform-modules/multi-container-service/discovery.tf diff --git a/flake.nix b/flake.nix index 46f8f06..eda25ef 100644 --- a/flake.nix +++ b/flake.nix @@ -17,6 +17,7 @@ terraform terragrunt tfautomv + ssm-session-manager-plugin ]; GIT_TEMPLATE_DIR=""; }; diff --git a/terraform-incubator/access-the-data/main.tf b/terraform-incubator/access-the-data/main.tf index f023143..9ae4ed3 100644 --- a/terraform-incubator/access-the-data/main.tf +++ b/terraform-incubator/access-the-data/main.tf @@ -27,15 +27,16 @@ module "database" { source = "../../terraform-modules/database" - environment = each.value.environment - db_name = each.value.db_name - username = "ckan" + shared_configuration = local.shared_configuration + environment = each.value.environment + db_name = each.value.db_name + username = "ckan" } module "secrets" { for_each = local.envs source = "../../terraform-modules/cheap-secrets" - scope-name = "ckan-${each.value}" + scope-name = "ckan-${each.key}" secret-names = ["csrf", "admin-password"] } @@ -65,15 +66,15 @@ module "access-the-data" { path_patterns = ["/*"] env_vars = merge({ DATABASE = "postgres" - POSTGRES_HOST = module.database.host - POSTGRES_PORT = module.database.port + POSTGRES_HOST = module.database[each.key].host + POSTGRES_PORT = module.database[each.key].port // SQLALCHEMY has been set up in the container = // we don't know the PG password, so we can't build the URLs # Taken verbatim from .env - CKAN_DB = module.database.database - CKAN_DB_USER = module.database.user + CKAN_DB = module.database[each.key].database + CKAN_DB_USER = module.database[each.key].user CKAN_VERSION = "2.10.0" CKAN_SITE_ID = "default" @@ -103,9 +104,9 @@ module "access-the-data" { CKAN__FAVICON = "favicon.png" }, lookup(each.value.container_env, "ckan", {})) secrets = { - CKAN_DB_PASSWORD = module.databse.password_arn - CKAN___BEAKER__SESSION__SECRET = module.secrets["ckan-${each.value}"].arn["csrf"] - CKAN_SYSADMIN_PASSWORD = module.secrets["ckan-${each.value}"].arn["admin-password"] + CKAN_DB_PASSWORD = module.database[each.key].password_arn + CKAN___BEAKER__SESSION__SECRET = module.secrets[each.key].arn["csrf"] + CKAN_SYSADMIN_PASSWORD = module.secrets[each.key].arn["admin-password"] } } diff --git a/terraform-modules/cheap-secrets/main.tf b/terraform-modules/cheap-secrets/main.tf index 30dc2a2..d120c74 100644 --- a/terraform-modules/cheap-secrets/main.tf +++ b/terraform-modules/cheap-secrets/main.tf @@ -1,3 +1,7 @@ +variable "scope-name" { + type = string +} + variable "secret-names" { type = list(string) } @@ -8,7 +12,7 @@ variable "length" { } data "aws_secretsmanager_random_password" "them" { - for_each = var.secret_names + for_each = toset(var.secret-names) password_length = var.length } @@ -17,8 +21,8 @@ data "aws_secretsmanager_random_password" "them" { // the value to be updated. We get most of the benefit of a // Secret Manager entry, and save 0.40 USD/mo resource "aws_ssm_parameter" "these" { - for_each = var.secret_names - name = each.value + for_each = toset(var.secret-names) + name = "${var.scope-name}-${each.value}" type = "SecureString" value = data.aws_secretsmanager_random_password.them[each.value].random_password lifecycle { diff --git a/terraform-modules/database/main.tf b/terraform-modules/database/main.tf index 1698adf..723313d 100644 --- a/terraform-modules/database/main.tf +++ b/terraform-modules/database/main.tf @@ -8,7 +8,8 @@ terraform { } data "aws_secretsmanager_random_password" "db_password_init" { - password_length = 48 + password_length = 48 + exclude_punctuation = true } data "aws_db_instance" "shared" { @@ -20,8 +21,8 @@ data "aws_db_instance" "shared" { // the value to be updated. We get most of the benefit of a // Secret Manager entry, and save 0.40 USD/mo resource "aws_ssm_parameter" "rds_dbowner_password" { - name = "rds_password_${var.db_name}_${var.environment}" - type = "String" + name = "app_rds_password_${var.db_name}_${var.environment}" + type = "SecureString" value = data.aws_secretsmanager_random_password.db_password_init.random_password lifecycle { ignore_changes = [value] diff --git a/terraform-modules/ecs-task/main.tf b/terraform-modules/ecs-task/main.tf index a51e24d..c397a49 100644 --- a/terraform-modules/ecs-task/main.tf +++ b/terraform-modules/ecs-task/main.tf @@ -126,7 +126,7 @@ resource "aws_ecs_service" "ec2" { count = var.launch_type == "FARGATE" ? 0 : 1 name = local.envappname cluster = var.shared_configuration.cluster_id - enable_execute_command = true + enable_execute_command = true // XXX should be false except for dev/QA task_definition = aws_ecs_task_definition.task.arn launch_type = var.launch_type desired_count = var.desired_count @@ -140,6 +140,10 @@ resource "aws_ecs_service" "ec2" { } } + service_registries { + registry_arn = service_registry.arn + } + depends_on = [aws_lb_listener_rule.static] // XXX put into module refs lifecycle { @@ -171,6 +175,10 @@ resource "aws_ecs_service" "fargate" { } } + service_registries { + registry_arn = service_registry.arn + } + depends_on = [aws_lb_listener_rule.static] lifecycle { diff --git a/terraform-modules/multi-container-service/discovery.tf b/terraform-modules/multi-container-service/discovery.tf new file mode 100644 index 0000000..9a54c02 --- /dev/null +++ b/terraform-modules/multi-container-service/discovery.tf @@ -0,0 +1,24 @@ +resource "aws_service_discovery_private_dns_namespace" "internal" { + name = local.discovery_domain + description = "Discovery for ${var.project_name} in ${var.environment}" + vpc = var.shared_configuration.vpc_id +} + +resource "aws_service_discovery_service" "internal" { + name = "${local.envname}-internal" + + dns_config { + namespace_id = aws_service_discovery_private_dns_namespace.internal.id + + dns_records { + ttl = 10 + type = "A" + } + + routing_policy = "MULTIVALUE" + } + + health_check_custom_config { + failure_threshold = 3 + } +} diff --git a/terraform-modules/multi-container-service/tasks.tf b/terraform-modules/multi-container-service/tasks.tf index 299f403..1049702 100644 --- a/terraform-modules/multi-container-service/tasks.tf +++ b/terraform-modules/multi-container-service/tasks.tf @@ -46,7 +46,7 @@ resource "aws_iam_role" "ecs_task_execution_role" { { Action = "ssm:GetParameters" Effect = "Allow" - Resource = [aws_ssm_parameter.rds_dbowner_password.arn] + Resource = flatten([for k, v in var.containers : values(v.secrets)]) } ] }) @@ -64,6 +64,12 @@ resource "aws_iam_role_policy_attachment" "ecs_task" { module "ecs-task" { for_each = var.containers + service_registry_arn = aws_service_discovery_service.internal.arn + // TODO In ecs-task module: + // + // service_registries { + // registry_arn = service_registry.arn + // } source = "../ecs-task" shared_configuration = var.shared_configuration @@ -80,11 +86,13 @@ module "ecs-task" { container_cpu = each.value.cpu container_memory = each.value.memory - host_names = [for s in each.value.subdomains : "${s}.${data.aws_route53_zone.this.name}"] - path_patterns = each.value.path_patterns - health_check_path = each.value.health_check_path - log_group = aws_cloudwatch_log_group.cwlogs.name - container_port = each.value.port - container_env_vars = each.value.env_vars - container_secrets = each.value.secrets + host_names = [for s in each.value.subdomains : "${s}.${data.aws_route53_zone.this.name}"] + path_patterns = each.value.path_patterns + health_check_path = each.value.health_check_path + log_group = aws_cloudwatch_log_group.cwlogs.name + container_port = each.value.port + container_env_vars = merge(each.value.env_vars, { + SERVICE_DISCOVERY_DOMAIN = local.discovery_domain + }) + container_secrets = each.value.secrets } diff --git a/terraform-modules/multi-container-service/variables.tf b/terraform-modules/multi-container-service/variables.tf index ce15cbc..a731afb 100644 --- a/terraform-modules/multi-container-service/variables.tf +++ b/terraform-modules/multi-container-service/variables.tf @@ -1,6 +1,7 @@ locals { - envname = "${var.project_name}-${var.environment}" - envappname = "${var.project_name}-${var.application_type}-${var.environment}" + envname = "${var.project_name}-${var.environment}" + envappname = "${var.project_name}-${var.application_type}-${var.environment}" + discovery_domain = "${local.envname}.local" } # c.f. terraform-incubator shared_configuration/all @@ -67,6 +68,7 @@ variable "containers" { path_patterns = optional(list(string), []) health_check_path = optional(string, "/") env_vars = optional(map(any), {}) + secrets = optional(map(any), {}) })) description = "Per container service configuration. Note that subdomains are used (e.g. 'www' not 'www.example.com')" From 646f3e26869431ea1b703c93e685cdbeba521a97 Mon Sep 17 00:00:00 2001 From: Judson Lester Date: Fri, 8 Dec 2023 15:07:03 -0800 Subject: [PATCH 4/4] Incubator is ready for accessthedata --- terraform-incubator/access-the-data/main.tf | 37 +++++++++---- terraform-modules/database/main.tf | 54 +++++++++++++++++-- terraform-modules/database/variables.tf | 35 ++++++++++-- terraform-modules/ecs-task/main.tf | 25 ++++++++- terraform-modules/ecs-task/variables.tf | 4 ++ .../multi-container-service/discovery.tf | 19 ------- .../multi-container-service/tasks.tf | 8 +-- terraform-modules/project-zone/main.tf | 37 +++++++++++++ 8 files changed, 174 insertions(+), 45 deletions(-) diff --git a/terraform-incubator/access-the-data/main.tf b/terraform-incubator/access-the-data/main.tf index 9ae4ed3..89cd901 100644 --- a/terraform-incubator/access-the-data/main.tf +++ b/terraform-incubator/access-the-data/main.tf @@ -6,7 +6,6 @@ locals { envs = { dev = { environment = "dev" - db_name = "access-the-data-dev" host_names = ["dev"] container_env = { CKAN_SITE_URL = "https://dev.accessthedata.org" @@ -19,6 +18,7 @@ module "zone" { source = "../../terraform-modules/project-zone" zone_name = "accessthedata.org" + github_at_apex = true shared_configuration = local.shared_configuration } @@ -29,8 +29,20 @@ module "database" { shared_configuration = local.shared_configuration environment = each.value.environment - db_name = each.value.db_name - username = "ckan" + db_name = "accessthedata" + owner_name = "ckan" +} + +module "datastore_database" { + for_each = local.envs + + source = "../../terraform-modules/database" + + shared_configuration = local.shared_configuration + environment = each.value.environment + db_name = "accessthedata_datastore" + owner_name = "ckands" + viewer_name = "ckands_ro" } module "secrets" { @@ -73,10 +85,13 @@ module "access-the-data" { // we don't know the PG password, so we can't build the URLs # Taken verbatim from .env - CKAN_DB = module.database[each.key].database - CKAN_DB_USER = module.database[each.key].user - CKAN_VERSION = "2.10.0" - CKAN_SITE_ID = "default" + CKAN_DB = module.database[each.key].database + CKAN_DB_USER = module.database[each.key].owner + CKAN_DATASTORE_DB = module.datastore_database[each.key].database + CKAN_DATASTORE_DB_RWUSER = module.datastore_database[each.key].owner + CKAN_DATASTORE_DB_ROUSER = module.datastore_database[each.key].viewer + CKAN_VERSION = "2.10.0" + CKAN_SITE_ID = "default" CKAN_PORT = "5000" CKAN_PORT_HOST = "5000" @@ -104,7 +119,9 @@ module "access-the-data" { CKAN__FAVICON = "favicon.png" }, lookup(each.value.container_env, "ckan", {})) secrets = { - CKAN_DB_PASSWORD = module.database[each.key].password_arn + CKAN_DB_PASSWORD = module.database[each.key].owner_password_arn + CKAN_DATASTORE_DB_RWPASSWORD = module.datastore_database[each.key].owner_password_arn + CKAN_DATASTORE_DB_ROPASSWORD = module.datastore_database[each.key].viewer_password_arn CKAN___BEAKER__SESSION__SECRET = module.secrets[each.key].arn["csrf"] CKAN_SYSADMIN_PASSWORD = module.secrets[each.key].arn["admin-password"] } @@ -118,8 +135,8 @@ module "access-the-data" { solr = { tag = "latest" - cpu = 256 - memory = 512 + cpu = 512 + memory = 4096 } redis = { diff --git a/terraform-modules/database/main.tf b/terraform-modules/database/main.tf index 723313d..979a05e 100644 --- a/terraform-modules/database/main.tf +++ b/terraform-modules/database/main.tf @@ -29,15 +29,61 @@ resource "aws_ssm_parameter" "rds_dbowner_password" { } } +resource "aws_ssm_parameter" "rds_dbuser_password" { + name = "app_rds_rw_password_${var.db_name}_${var.environment}" + type = "SecureString" + value = data.aws_secretsmanager_random_password.db_password_init.random_password + lifecycle { + ignore_changes = [value] + } +} + +resource "aws_ssm_parameter" "rds_dbviewer_password" { + name = "app_rds_ro_password_${var.db_name}_${var.environment}" + type = "SecureString" + value = data.aws_secretsmanager_random_password.db_password_init.random_password + lifecycle { + ignore_changes = [value] + } +} + resource "postgresql_role" "db_owner" { - count = var.username != "" ? 1 : 0 - name = "${var.username}_${var.environment}" + name = "${var.owner_name}_${var.environment}" login = true password = aws_ssm_parameter.rds_dbowner_password.value } resource "postgresql_database" "db" { - count = var.db_name != "" ? 1 : 0 name = "${var.db_name}_${var.environment}" - owner = postgresql_role.db_owner[0].name + owner = postgresql_role.db_owner.name +} + +resource "postgresql_role" "db_user" { + count = var.user_name != "" ? 1 : 0 + name = "${var.user_name}_${var.environment}" + login = true + password = aws_ssm_parameter.rds_dbuser_password.value +} + +resource "postgresql_grant" "user" { + count = var.user_name != "" ? 1 : 0 + database = postgresql_database.db.name + role = postgresql_role.db_user[0].name + object_type = "table" + privileges = ["SELECT", "INSERT", "UPDATE", "DELETE"] +} + +resource "postgresql_role" "db_viewer" { + count = var.viewer_name != "" ? 1 : 0 + name = "${var.viewer_name}_${var.environment}" + login = true + password = aws_ssm_parameter.rds_dbviewer_password.value +} + +resource "postgresql_grant" "viewer" { + count = var.user_name != "" ? 1 : 0 + database = postgresql_database.db.name + role = postgresql_role.db_viewer[0].name + object_type = "table" + privileges = ["SELECT"] } diff --git a/terraform-modules/database/variables.tf b/terraform-modules/database/variables.tf index f753f0a..36f107c 100644 --- a/terraform-modules/database/variables.tf +++ b/terraform-modules/database/variables.tf @@ -20,10 +20,21 @@ variable "db_name" { type = string } -variable "username" { +variable "owner_name" { type = string } +variable "user_name" { + type = string + default = "" +} + +variable "viewer_name" { + type = string + default = "" +} + + output "host" { value = data.aws_db_instance.shared.address } @@ -33,13 +44,29 @@ output "port" { } output "database" { - value = postgresql_database.db[0].name + value = postgresql_database.db.name +} + +output "owner" { + value = postgresql_role.db_owner.name } output "user" { - value = postgresql_role.db_owner[0].name + value = length(postgresql_role.db_user) > 0 ? postgresql_role.db_user[0].name : "UNSET" } -output "password_arn" { +output "viewer" { + value = length(postgresql_role.db_viewer) > 0 ? postgresql_role.db_viewer[0].name : "UNSET" +} + +output "owner_password_arn" { value = aws_ssm_parameter.rds_dbowner_password.arn } + +output "user_password_arn" { + value = aws_ssm_parameter.rds_dbuser_password.arn +} + +output "viewer_password_arn" { + value = aws_ssm_parameter.rds_dbviewer_password.arn +} diff --git a/terraform-modules/ecs-task/main.tf b/terraform-modules/ecs-task/main.tf index c397a49..56afdf2 100644 --- a/terraform-modules/ecs-task/main.tf +++ b/terraform-modules/ecs-task/main.tf @@ -122,6 +122,27 @@ resource "aws_lb_listener_rule" "static" { } } } + + +resource "aws_service_discovery_service" "internal" { + name = var.application_type + + dns_config { + namespace_id = var.service_discovery_dns_namespace_id + + dns_records { + ttl = 10 + type = "A" + } + + routing_policy = "MULTIVALUE" + } + + health_check_custom_config { + failure_threshold = 3 + } +} + resource "aws_ecs_service" "ec2" { count = var.launch_type == "FARGATE" ? 0 : 1 name = local.envappname @@ -141,7 +162,7 @@ resource "aws_ecs_service" "ec2" { } service_registries { - registry_arn = service_registry.arn + registry_arn = aws_service_discovery_service.internal.arn } depends_on = [aws_lb_listener_rule.static] // XXX put into module refs @@ -176,7 +197,7 @@ resource "aws_ecs_service" "fargate" { } service_registries { - registry_arn = service_registry.arn + registry_arn = aws_service_discovery_service.internal.arn } depends_on = [aws_lb_listener_rule.static] diff --git a/terraform-modules/ecs-task/variables.tf b/terraform-modules/ecs-task/variables.tf index e70e803..2724a6c 100644 --- a/terraform-modules/ecs-task/variables.tf +++ b/terraform-modules/ecs-task/variables.tf @@ -89,3 +89,7 @@ variable "health_check_path" { variable "log_group" { type = string } + +variable "service_discovery_dns_namespace_id" { + type = string +} diff --git a/terraform-modules/multi-container-service/discovery.tf b/terraform-modules/multi-container-service/discovery.tf index 9a54c02..f03c43a 100644 --- a/terraform-modules/multi-container-service/discovery.tf +++ b/terraform-modules/multi-container-service/discovery.tf @@ -3,22 +3,3 @@ resource "aws_service_discovery_private_dns_namespace" "internal" { description = "Discovery for ${var.project_name} in ${var.environment}" vpc = var.shared_configuration.vpc_id } - -resource "aws_service_discovery_service" "internal" { - name = "${local.envname}-internal" - - dns_config { - namespace_id = aws_service_discovery_private_dns_namespace.internal.id - - dns_records { - ttl = 10 - type = "A" - } - - routing_policy = "MULTIVALUE" - } - - health_check_custom_config { - failure_threshold = 3 - } -} diff --git a/terraform-modules/multi-container-service/tasks.tf b/terraform-modules/multi-container-service/tasks.tf index 1049702..f201a5e 100644 --- a/terraform-modules/multi-container-service/tasks.tf +++ b/terraform-modules/multi-container-service/tasks.tf @@ -64,12 +64,8 @@ resource "aws_iam_role_policy_attachment" "ecs_task" { module "ecs-task" { for_each = var.containers - service_registry_arn = aws_service_discovery_service.internal.arn - // TODO In ecs-task module: - // - // service_registries { - // registry_arn = service_registry.arn - // } + service_discovery_dns_namespace_id = aws_service_discovery_private_dns_namespace.internal.id + source = "../ecs-task" shared_configuration = var.shared_configuration diff --git a/terraform-modules/project-zone/main.tf b/terraform-modules/project-zone/main.tf index 5b4b025..d9194e1 100644 --- a/terraform-modules/project-zone/main.tf +++ b/terraform-modules/project-zone/main.tf @@ -10,6 +10,11 @@ variable "shared_configuration" { }) } +variable "github_at_apex" { + type = bool + default = false +} + output "zone_id" { value = aws_route53_zone.this.zone_id } @@ -24,6 +29,7 @@ data "aws_lb" "lb" { } resource "aws_route53_record" "apex" { + count = var.github_at_apex ? 0 : 1 zone_id = aws_route53_zone.this.zone_id name = aws_route53_zone.this.name type = "A" @@ -35,6 +41,37 @@ resource "aws_route53_record" "apex" { } } +// Per https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-an-apex-domain +resource "aws_route53_record" "apex-gh" { + count = var.github_at_apex ? 1 : 0 + zone_id = aws_route53_zone.this.zone_id + name = aws_route53_zone.this.name + type = "A" + ttl = 3600 + + records = [ + "185.199.108.153", + "185.199.109.153", + "185.199.110.153", + "185.199.111.153", + ] +} + +resource "aws_route53_record" "apex-gh-ipv6" { + count = var.github_at_apex ? 1 : 0 + zone_id = aws_route53_zone.this.zone_id + name = aws_route53_zone.this.name + type = "AAAA" + ttl = 3600 + + records = [ + "2606:50c0:8000::153", + "2606:50c0:8001::153", + "2606:50c0:8002::153", + "2606:50c0:8003::153", + ] +} + resource "aws_acm_certificate" "domain" { domain_name = var.zone_name