From a02e378652f22e0a61078011b573ee3ee565ec49 Mon Sep 17 00:00:00 2001 From: Rocket ! Date: Mon, 13 Nov 2023 16:26:09 -0800 Subject: [PATCH 1/3] Create small other app for testing purposes --- infra/app/service/main.tf | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/infra/app/service/main.tf b/infra/app/service/main.tf index f4c000eb..47d434c3 100644 --- a/infra/app/service/main.tf +++ b/infra/app/service/main.tf @@ -62,6 +62,19 @@ module "app_config" { source = "../app-config" } +data "aws_security_groups" "aws_services" { + filter { + name = "group-name" + values = ["${module.project_config.aws_services_security_group_name_prefix}*"] + } + + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + + data "aws_rds_cluster" "db_cluster" { count = module.app_config.has_database ? 1 : 0 cluster_identifier = local.database_config.cluster_name @@ -84,13 +97,25 @@ data "aws_ssm_parameter" "incident_management_service_integration_url" { name = local.incident_management_service_integration_config.integration_url_param_name } +# Customizations: Retrieve passwords created elsewhere (by other modules, manually) + +data "aws_ssm_parameter" "custom_secret" { + name = "/custom_secret" +} + module "service" { - source = "../../modules/service" - service_name = local.service_name - image_repository_name = module.app_config.image_repository_name - image_tag = local.image_tag - vpc_id = data.aws_vpc.default.id - subnet_ids = data.aws_subnets.default.ids + source = "../../modules/service" + service_name = local.service_name + image_repository_name = module.app_config.image_repository_name + image_tag = "latest" + external_image_url = "docker.io/rocketnovadockerhub/tiny-env-test" + container_read_only = false + container_port = 8000 + healthcheck_start_period = 0 + healthcheck_path = "customhealth" + healthcheck_matcher = "200-302" + vpc_id = data.aws_vpc.default.id + subnet_ids = data.aws_subnets.default.ids db_vars = module.app_config.has_database ? { security_group_ids = data.aws_rds_cluster.db_cluster[0].vpc_security_group_ids @@ -104,6 +129,15 @@ module "service" { schema_name = local.database_config.schema_name } } : null + + container_env_vars = [ + { name : "CUSTOM_ENV_VAR", value : "100" }, + ] + container_secrets = [ + { name : "CUSTOM_SECRET", valueFrom : data.aws_ssm_parameter.custom_secret.arn }, + ] + aws_services_security_group_id = data.aws_security_groups.aws_services.ids[0] + } module "monitoring" { From ff5aea10f35b11a8a2885cb396ca79709aa635f2 Mon Sep 17 00:00:00 2001 From: Rocket ! Date: Mon, 13 Nov 2023 17:12:46 -0800 Subject: [PATCH 2/3] Add custom secret for automated tests --- infra/app/service/main.tf | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/infra/app/service/main.tf b/infra/app/service/main.tf index 47d434c3..8def8aa1 100644 --- a/infra/app/service/main.tf +++ b/infra/app/service/main.tf @@ -14,8 +14,8 @@ data "aws_subnets" "default" { locals { # The prefix key/value pair is used for Terraform Workspaces, which is useful for projects with multiple infrastructure developers. - # By default, Terraform creates a workspace named “default.” If a non-default workspace is not created this prefix will equal “default”, - # if you choose not to use workspaces set this value to "dev" + # By default, Terraform creates a workspace named “default.” If a non-default workspace is not created this prefix will equal “default”, + # if you choose not to use workspaces set this value to "dev" prefix = terraform.workspace == "default" ? "" : "${terraform.workspace}-" # Add environment specific tags @@ -99,8 +99,10 @@ data "aws_ssm_parameter" "incident_management_service_integration_url" { # Customizations: Retrieve passwords created elsewhere (by other modules, manually) -data "aws_ssm_parameter" "custom_secret" { - name = "/custom_secret" +resource "aws_ssm_parameter" "custom_secret" { + name = "/custom_secret" + type = "SecureString" + value = "200" } module "service" { @@ -134,7 +136,7 @@ module "service" { { name : "CUSTOM_ENV_VAR", value : "100" }, ] container_secrets = [ - { name : "CUSTOM_SECRET", valueFrom : data.aws_ssm_parameter.custom_secret.arn }, + { name : "CUSTOM_SECRET", valueFrom : aws_ssm_parameter.custom_secret.arn }, ] aws_services_security_group_id = data.aws_security_groups.aws_services.ids[0] From b780c1e167d87cd2562d7cf525ef858acc64b94e Mon Sep 17 00:00:00 2001 From: Rocket ! Date: Mon, 13 Nov 2023 16:43:26 -0800 Subject: [PATCH 3/3] Add changes from infra-template PR 474 --- infra/modules/service/access-control.tf | 33 +++++++++--- infra/modules/service/load-balancer.tf | 4 +- infra/modules/service/main.tf | 34 ++++++++----- infra/modules/service/networking.tf | 10 ++++ infra/modules/service/variables.tf | 68 +++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 22 deletions(-) diff --git a/infra/modules/service/access-control.tf b/infra/modules/service/access-control.tf index 0b295088..c4445b0d 100644 --- a/infra/modules/service/access-control.tf +++ b/infra/modules/service/access-control.tf @@ -53,14 +53,31 @@ data "aws_iam_policy_document" "task_executor" { } # Allow ECS to download images. - statement { - sid = "ECRPullAccess" - actions = [ - "ecr:BatchCheckLayerAvailability", - "ecr:BatchGetImage", - "ecr:GetDownloadUrlForLayer", - ] - resources = [data.aws_ecr_repository.app.arn] + dynamic "statement" { + for_each = var.external_image_url == "" ? [true] : [] + content { + sid = "ECRPullAccess" + actions = [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + ] + resources = [data.aws_ecr_repository.app[0].arn] + } + } + + # Allow ECS to access Parameter Store for specific resources + # But only include the statement if var.container_secrets is not empty + # Strip any non-alphanumeric values from the secret name + dynamic "statement" { + for_each = var.container_secrets + content { + sid = "SSMAccess${replace(statement.value.name, "/[^0-9A-Za-z]/", "")}" + actions = [ + "ssm:GetParameters", + ] + resources = [statement.value.valueFrom] + } } } diff --git a/infra/modules/service/load-balancer.tf b/infra/modules/service/load-balancer.tf index 6ec73e06..f78f041d 100644 --- a/infra/modules/service/load-balancer.tf +++ b/infra/modules/service/load-balancer.tf @@ -81,13 +81,13 @@ resource "aws_lb_target_group" "app_tg" { deregistration_delay = "30" health_check { - path = "/health" + path = "/${local.healthcheck_path}" port = var.container_port healthy_threshold = 2 unhealthy_threshold = 10 interval = 30 timeout = 29 - matcher = "200-299" + matcher = var.healthcheck_matcher } lifecycle { diff --git a/infra/modules/service/main.tf b/infra/modules/service/main.tf index 9eca27e4..ce50d848 100644 --- a/infra/modules/service/main.tf +++ b/infra/modules/service/main.tf @@ -1,7 +1,8 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} data "aws_ecr_repository" "app" { - name = var.image_repository_name + count = var.external_image_url == "" ? 1 : 0 + name = var.image_repository_name } locals { @@ -10,12 +11,15 @@ locals { log_group_name = "service/${var.service_name}" log_stream_prefix = var.service_name task_executor_role_name = "${var.service_name}-task-executor" - image_url = "${data.aws_ecr_repository.app.repository_url}:${var.image_tag}" + image_url = var.external_image_url != "" ? "${var.external_image_url}:${var.image_tag}" : "${data.aws_ecr_repository.app[0].repository_url}:${var.image_tag}" + healthcheck_path = trimprefix(var.healthcheck_path, "/") + read_only = var.container_read_only base_environment_variables = [ { name : "PORT", value : tostring(var.container_port) }, { name : "AWS_REGION", value : data.aws_region.current.name }, ] + db_environment_variables = var.db_vars == null ? [] : [ { name : "DB_HOST", value : var.db_vars.connection_info.host }, { name : "DB_PORT", value : var.db_vars.connection_info.port }, @@ -23,7 +27,9 @@ locals { { name : "DB_NAME", value : var.db_vars.connection_info.db_name }, { name : "DB_SCHEMA", value : var.db_vars.connection_info.schema_name }, ] - environment_variables = concat(local.base_environment_variables, local.db_environment_variables) + + environment_variables = concat(local.base_environment_variables, local.db_environment_variables, var.container_env_vars) + container_secrets = var.container_secrets } #------------------- @@ -71,20 +77,23 @@ resource "aws_ecs_task_definition" "app" { cpu = var.cpu, networkMode = "awsvpc", essential = true, - readonlyRootFilesystem = true, + readonlyRootFilesystem = local.read_only, # Need to define all parameters in the healthCheck block even if we want # to use AWS's defaults, otherwise the terraform plan will show a diff # that will force a replacement of the task definition - healthCheck = { - interval = 30, - retries = 3, - timeout = 5, - command = ["CMD-SHELL", - "wget --no-verbose --tries=1 --spider http://localhost:${var.container_port}/health || exit 1" - ] - }, + healthCheck = var.enable_container_healthcheck ? { + interval = 30, + retries = 3, + timeout = 5, + startPeriod = var.healthcheck_start_period, + command = [ + "CMD-SHELL", + var.healthcheck_type == "curl" ? "curl --fail http://localhost:${var.container_port}/${local.healthcheck_path} || exit 1" : "wget --no-verbose --tries=1 --spider http://localhost:${var.container_port}/${local.healthcheck_path} || exit 1", + ], + } : null, environment = local.environment_variables, + secrets = local.container_secrets, portMappings = [ { containerPort = var.container_port, @@ -124,3 +133,4 @@ resource "aws_ecs_cluster" "cluster" { value = "enabled" } } + diff --git a/infra/modules/service/networking.tf b/infra/modules/service/networking.tf index 0c74929d..a44dc20b 100644 --- a/infra/modules/service/networking.tf +++ b/infra/modules/service/networking.tf @@ -66,3 +66,13 @@ resource "aws_security_group" "app" { cidr_blocks = ["0.0.0.0/0"] } } + +resource "aws_vpc_security_group_ingress_rule" "vpc_endpoints_ingress_from_app" { + security_group_id = var.aws_services_security_group_id + description = "Allow inbound requests to VPC endpoints from application ${var.service_name}" + + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + referenced_security_group_id = aws_security_group.app.id +} diff --git a/infra/modules/service/variables.tf b/infra/modules/service/variables.tf index 043f2cf7..1d55c1da 100644 --- a/infra/modules/service/variables.tf +++ b/infra/modules/service/variables.tf @@ -16,6 +16,12 @@ variable "image_repository_name" { description = "The name of the container image repository" } +variable "external_image_url" { + type = string + description = "A non-AWS container image repository. If this is not empty, this takes precedence over image_repository_name" + default = "" +} + variable "desired_instance_count" { type = number description = "Number of instances of the task definition to place and keep running." @@ -67,3 +73,65 @@ variable "db_vars" { }) default = null } + +variable "container_env_vars" { + type = list(map(string)) + description = "Additional environment variables to pass to the container definition" + default = [] +} + +variable "container_secrets" { + type = list(map(string)) + description = "AWS secrets to pass to the container definition" + default = [] +} + +variable "aws_services_security_group_id" { + type = string + description = "Security group ID for VPC endpoints that access AWS Services" +} + + +variable "container_read_only" { + type = bool + description = "Whether the container root filesystem should be read-only" + default = true +} + +#------------------- +# Healthcheck +#------------------- + +variable "healthcheck_path" { + type = string + description = "The path to the application healthcheck" + default = "/health" +} + +variable "healthcheck_type" { + type = string + description = "Whether to configure a curl or wget healthcheck. curl is more common. use wget for alpine-based images" + default = "wget" + validation { + condition = contains(["curl", "wget"], var.healthcheck_type) + error_message = "choose either: curl or wget" + } +} + +variable "healthcheck_matcher" { + type = string + description = "The response codes that indicate healthy to the ALB" + default = "200-299" +} + +variable "healthcheck_start_period" { + type = number + description = "The optional grace period to provide containers time to bootstrap in before failed health checks count towards the maximum number of retries" + default = 0 +} + +variable "enable_container_healthcheck" { + type = bool + description = "The ALB healthcheck is mandatory, but the container healthcheck can be disabled if desired" + default = true +}