diff --git a/examples/braintrust-data-plane-external-eks-quarantine/main.tf b/examples/braintrust-data-plane-external-eks-quarantine/main.tf index e88051a2..2ec34bf1 100644 --- a/examples/braintrust-data-plane-external-eks-quarantine/main.tf +++ b/examples/braintrust-data-plane-external-eks-quarantine/main.tf @@ -32,7 +32,12 @@ module "braintrust-data-plane" { # for forward compatibility. allowed_org_ids = "" - # Brainstore license key (required) + # Optional in external-EKS mode (Brainstore EC2 is not created). Defaults to + # null; only needed if you also enable create_ai_gateway (gateway telemetry). + # CI runs `validate` on this example with the key unset (null default), + # exercising null-default wiring for syntax/type/reference errors. Note: + # `validate` does NOT catch eval-time errors (e.g. coalesce-on-null) — a + # `terraform test` (command=plan) guard for that is tracked separately. brainstore_license_key = var.brainstore_license_key # Enable external EKS deployment mode diff --git a/examples/braintrust-data-plane-external-eks-quarantine/variables.tf b/examples/braintrust-data-plane-external-eks-quarantine/variables.tf index 018629d5..e9d75f16 100644 --- a/examples/braintrust-data-plane-external-eks-quarantine/variables.tf +++ b/examples/braintrust-data-plane-external-eks-quarantine/variables.tf @@ -1,6 +1,6 @@ variable "brainstore_license_key" { - description = "The license key for the Brainstore instance. You can get this from the Braintrust UI in Settings > Data Plane." + description = "Optional. Only needed if you enable create_ai_gateway in external-EKS mode (Brainstore EC2 is not created)." type = string - # Note: For actual deployments, set this via environment variable or terraform.tfvars - # For testing: TF_VAR_brainstore_license_key=dummy terraform validate + default = null + sensitive = true } diff --git a/examples/braintrust-data-plane-sandbox/variables.tf b/examples/braintrust-data-plane-sandbox/variables.tf index 79341eee..7b13b0dc 100644 --- a/examples/braintrust-data-plane-sandbox/variables.tf +++ b/examples/braintrust-data-plane-sandbox/variables.tf @@ -1,6 +1,7 @@ variable "brainstore_license_key" { description = "The license key for the Brainstore instance. You can get this from the Braintrust UI in Settings > Data Plane." type = string + sensitive = true validation { condition = var.brainstore_license_key != null && var.brainstore_license_key != "" error_message = "The brainstore_license_key must be set." diff --git a/examples/braintrust-data-plane/variables.tf b/examples/braintrust-data-plane/variables.tf index 58d8f791..abdbaea3 100644 --- a/examples/braintrust-data-plane/variables.tf +++ b/examples/braintrust-data-plane/variables.tf @@ -1,6 +1,7 @@ variable "brainstore_license_key" { description = "The license key for the Brainstore instance. You can get this from the Braintrust UI in Settings > Data Plane." type = string + sensitive = true validation { condition = var.brainstore_license_key != null && var.brainstore_license_key != "" error_message = "The brainstore_license_key must be set." diff --git a/main.tf b/main.tf index c769e5ca..8c683a9a 100644 --- a/main.tf +++ b/main.tf @@ -49,12 +49,27 @@ locals { local.main_vpc_private_subnet_3_id ] - create_ecs_api = !var.use_deployment_mode_external_eks - enable_ecs_api = local.create_ecs_api && var.enable_ecs_api - create_ai_gateway = var.create_ai_gateway - enable_ai_gateway = local.create_ai_gateway && var.enable_ai_gateway - enable_internal_observability = trimspace(nonsensitive(var.internal_observability_api_key)) != "" - create_internal_observability_secret = local.enable_internal_observability && (local.create_ecs_api || local.create_ai_gateway) + create_ecs_api = !var.use_deployment_mode_external_eks + enable_ecs_api = local.create_ecs_api && var.enable_ecs_api + create_ai_gateway = var.create_ai_gateway + enable_ai_gateway = local.create_ai_gateway && var.enable_ai_gateway + create_brainstore_ec2 = var.enable_brainstore && !var.use_deployment_mode_external_eks + + enable_internal_observability = trimspace(nonsensitive(var.internal_observability_api_key)) != "" + # Secret is shared by API ECS, gateway ECS, and Brainstore EC2 (Datadog agent). + create_internal_observability_secret = local.enable_internal_observability && ( + local.create_ecs_api || local.create_ai_gateway || local.create_brainstore_ec2 + ) + brainstore_license_key_provided = var.brainstore_license_key == null ? false : trimspace(nonsensitive(var.brainstore_license_key)) != "" + # Stored in Secrets Manager so Brainstore user_data / gateway task defs never embed the key. + create_brainstore_license_secret = local.brainstore_license_key_provided && ( + local.create_brainstore_ec2 || local.create_ai_gateway + ) + brainstore_license_key_secret_arn = local.create_brainstore_license_secret ? aws_secretsmanager_secret.brainstore_license_key[0].arn : "" + brainstore_license_key_secret_version = local.create_brainstore_license_secret ? aws_secretsmanager_secret_version.brainstore_license_key[0].version_id : "" + internal_observability_api_key_secret_arn = local.create_internal_observability_secret ? aws_secretsmanager_secret.internal_observability_api_key[0].arn : "" + internal_observability_api_key_secret_version = local.create_internal_observability_secret ? aws_secretsmanager_secret_version.internal_observability_api_key[0].version_id : "" + ai_proxy_url_ssm_parameter_name = "/braintrust/${var.deployment_name}/ai-proxy-url" api_ecs_url_ssm_parameter_name = "/braintrust/${var.deployment_name}/ecs-api-url" brainstore_ai_proxy_url_ssm_parameter_name = local.enable_ecs_api ? local.api_ecs_url_ssm_parameter_name : local.ai_proxy_url_ssm_parameter_name @@ -351,34 +366,37 @@ module "gateway_ecs" { "public.ecr.aws/braintrust/gateway:%s", var.ai_gateway_version_override != null ? var.ai_gateway_version_override : jsondecode(file("${path.module}/modules/gateway-ecs/VERSIONS.json"))["gateway"] ) - cpu = var.ai_gateway_cpu - memory = var.ai_gateway_memory - cpu_architecture = var.ai_gateway_cpu_architecture - min_capacity = var.ai_gateway_min_capacity - max_capacity = var.ai_gateway_max_capacity - target_cpu_utilization = var.ai_gateway_target_cpu_utilization - target_memory_utilization = var.ai_gateway_target_memory_utilization - log_retention_days = var.ai_gateway_log_retention_days - permissions_boundary_arn = var.permissions_boundary_arn - use_redis_replication_group = var.use_redis_replication_group - redis_host = module.redis.redis_endpoint - redis_port = module.redis.redis_port - redis_security_group_id = module.redis.redis_security_group_id - target_group_arn = module.gateway_alb[0].gateway_target_group_arn - alb_security_group_id = module.gateway_alb[0].gateway_alb_security_group_id - gateway_http_listener_arn = module.gateway_alb[0].gateway_http_listener_arn - extra_env_vars = var.ai_gateway_extra_env_vars - custom_tags = local.all_custom_tags - brainstore_license_key = var.brainstore_license_key - enable_execute_command = var.ai_gateway_enable_execute_command - braintrust_app_url = var.ai_gateway_braintrust_app_url - braintrust_api_url = var.use_deployment_mode_external_eks ? var.braintrust_api_url : module.ingress[0].api_url - unsafe_url_request_mode = var.unsafe_url_request_mode - url_security_dns_servers = var.url_security_dns_servers - url_security_allow_cidrs = var.url_security_allow_cidrs + cpu = var.ai_gateway_cpu + memory = var.ai_gateway_memory + cpu_architecture = var.ai_gateway_cpu_architecture + min_capacity = var.ai_gateway_min_capacity + max_capacity = var.ai_gateway_max_capacity + target_cpu_utilization = var.ai_gateway_target_cpu_utilization + target_memory_utilization = var.ai_gateway_target_memory_utilization + log_retention_days = var.ai_gateway_log_retention_days + permissions_boundary_arn = var.permissions_boundary_arn + use_redis_replication_group = var.use_redis_replication_group + redis_host = module.redis.redis_endpoint + redis_port = module.redis.redis_port + redis_security_group_id = module.redis.redis_security_group_id + target_group_arn = module.gateway_alb[0].gateway_target_group_arn + alb_security_group_id = module.gateway_alb[0].gateway_alb_security_group_id + gateway_http_listener_arn = module.gateway_alb[0].gateway_http_listener_arn + extra_env_vars = var.ai_gateway_extra_env_vars + custom_tags = local.all_custom_tags + brainstore_license_key_enabled = local.create_brainstore_license_secret + brainstore_license_key_secret_arn = local.brainstore_license_key_secret_arn + brainstore_license_key_secret_version = local.brainstore_license_key_secret_version + enable_execute_command = var.ai_gateway_enable_execute_command + braintrust_app_url = var.ai_gateway_braintrust_app_url + braintrust_api_url = var.use_deployment_mode_external_eks ? var.braintrust_api_url : module.ingress[0].api_url + unsafe_url_request_mode = var.unsafe_url_request_mode + url_security_dns_servers = var.url_security_dns_servers + url_security_allow_cidrs = var.url_security_allow_cidrs # Observability - internal_observability_api_key_secret_arn = local.create_internal_observability_secret ? aws_secretsmanager_secret.internal_observability_api_key[0].arn : "" + internal_observability_api_key_secret_arn = local.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = local.internal_observability_api_key_secret_version internal_observability_enabled = local.create_internal_observability_secret internal_observability_env_name = var.internal_observability_env_name internal_observability_region = var.internal_observability_region @@ -394,7 +412,9 @@ module "api_ecs" { # Telemetry monitoring_telemetry = var.monitoring_telemetry - internal_observability_api_key_secret_arn = local.create_internal_observability_secret ? aws_secretsmanager_secret.internal_observability_api_key[0].arn : "" + internal_observability_api_key_secret_arn = local.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = local.internal_observability_api_key_secret_version + internal_observability_enabled = local.create_internal_observability_secret internal_observability_env_name = var.internal_observability_env_name internal_observability_region = var.internal_observability_region internal_observability_trace_disabled_plugins = var.internal_observability_trace_disabled_plugins @@ -528,6 +548,8 @@ module "services_common" { vpc_id = local.main_vpc_id kms_key_arn = local.kms_key_arn database_secret_arn = module.database.postgres_database_secret_arn + brainstore_license_key_secret_arn = local.brainstore_license_key_secret_arn + internal_observability_api_key_secret_arn = local.internal_observability_api_key_secret_arn brainstore_s3_bucket_arn = module.storage.brainstore_bucket_arn code_bundle_s3_bucket_arn = module.storage.code_bundle_bucket_arn lambda_responses_s3_bucket_arn = module.storage.lambda_responses_bucket_arn @@ -550,42 +572,44 @@ module "services_common" { module "brainstore" { source = "./modules/brainstore-ec2" - count = var.enable_brainstore && !var.use_deployment_mode_external_eks ? 1 : 0 - - deployment_name = var.deployment_name - instance_count = var.brainstore_instance_count - instance_type = var.brainstore_instance_type - instance_key_pair_name = var.brainstore_instance_key_pair_name - port = var.brainstore_port - license_key = var.brainstore_license_key - version_override = var.brainstore_version_override - skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects - brainstore_enable_export = var.brainstore_enable_export - extra_env_vars = var.brainstore_extra_env_vars - extra_env_vars_writer = var.brainstore_extra_env_vars_writer - writer_instance_count = var.brainstore_writer_instance_count - writer_instance_type = var.brainstore_writer_instance_type - fast_reader_instance_count = var.brainstore_fast_reader_instance_count - fast_reader_instance_type = var.brainstore_fast_reader_instance_type - extra_env_vars_fast_reader = var.brainstore_extra_env_vars_fast_reader - cache_file_size_fast_reader = var.brainstore_cache_file_size_fast_reader - ai_proxy_url_ssm_parameter = local.brainstore_ai_proxy_url_ssm_parameter - monitoring_telemetry = var.monitoring_telemetry - database_host = module.database.postgres_database_address - database_port = module.database.postgres_database_port - database_secret_arn = module.database.postgres_database_secret_arn - use_redis_replication_group = var.use_redis_replication_group - redis_host = module.redis.redis_endpoint - redis_port = module.redis.redis_port - service_token_secret_arn = module.services_common.function_tools_secret_arn - brainstore_s3_bucket_arn = module.storage.brainstore_bucket_arn - lambda_responses_s3_bucket_arn = module.storage.lambda_responses_bucket_arn - code_bundle_s3_bucket_arn = module.storage.code_bundle_bucket_arn - internal_observability_api_key = var.internal_observability_api_key - internal_observability_env_name = var.internal_observability_env_name - internal_observability_region = var.internal_observability_region - brainstore_instance_security_group_id = module.services_common.brainstore_instance_security_group_id - vpc_id = local.main_vpc_id + count = local.create_brainstore_ec2 ? 1 : 0 + + deployment_name = var.deployment_name + instance_count = var.brainstore_instance_count + instance_type = var.brainstore_instance_type + instance_key_pair_name = var.brainstore_instance_key_pair_name + port = var.brainstore_port + license_key_secret_arn = local.brainstore_license_key_secret_arn + license_key_secret_version = local.brainstore_license_key_secret_version + version_override = var.brainstore_version_override + skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects + brainstore_enable_export = var.brainstore_enable_export + extra_env_vars = var.brainstore_extra_env_vars + extra_env_vars_writer = var.brainstore_extra_env_vars_writer + writer_instance_count = var.brainstore_writer_instance_count + writer_instance_type = var.brainstore_writer_instance_type + fast_reader_instance_count = var.brainstore_fast_reader_instance_count + fast_reader_instance_type = var.brainstore_fast_reader_instance_type + extra_env_vars_fast_reader = var.brainstore_extra_env_vars_fast_reader + cache_file_size_fast_reader = var.brainstore_cache_file_size_fast_reader + ai_proxy_url_ssm_parameter = local.brainstore_ai_proxy_url_ssm_parameter + monitoring_telemetry = var.monitoring_telemetry + database_host = module.database.postgres_database_address + database_port = module.database.postgres_database_port + database_secret_arn = module.database.postgres_database_secret_arn + use_redis_replication_group = var.use_redis_replication_group + redis_host = module.redis.redis_endpoint + redis_port = module.redis.redis_port + service_token_secret_arn = module.services_common.function_tools_secret_arn + brainstore_s3_bucket_arn = module.storage.brainstore_bucket_arn + lambda_responses_s3_bucket_arn = module.storage.lambda_responses_bucket_arn + code_bundle_s3_bucket_arn = module.storage.code_bundle_bucket_arn + internal_observability_api_key_secret_arn = local.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = local.internal_observability_api_key_secret_version + internal_observability_env_name = var.internal_observability_env_name + internal_observability_region = var.internal_observability_region + brainstore_instance_security_group_id = module.services_common.brainstore_instance_security_group_id + vpc_id = local.main_vpc_id authorized_security_groups = merge( merge( { @@ -614,4 +638,8 @@ module "brainstore" { cache_file_size_reader = var.brainstore_cache_file_size_reader cache_file_size_writer = var.brainstore_cache_file_size_writer locks_s3_path = var.brainstore_locks_s3_path + + # Ensure brainstore IAM GetSecretValue grants (license / observability / etc.) + # are applied before launch templates that fetch those secrets in user_data. + depends_on = [module.services_common] } diff --git a/mise.toml b/mise.toml index f0f4ca92..a9fd0d31 100644 --- a/mise.toml +++ b/mise.toml @@ -14,44 +14,72 @@ precommit = ["pre-commit run --all-files"] description = "Validate the Terraform module and example code" run = """ #!/usr/bin/env bash -set -e -echo "Validating module" -terraform init -upgrade -terraform validate +set -euo pipefail -echo "Validating example code" -cd examples/braintrust-data-plane -# Override the module source to point to the local module. -# '*_override.tf' is a lesser known native terraform feature -trap 'rm -f main_override.tf' EXIT -cat < main_override.tf +validate_example() { + local example_dir="$1" + local label="$2" + echo "Validating ${label}" + ( + cd "${example_dir}" + # Override the module source to point to the local module. + # '*_override.tf' is a lesser known native terraform feature + trap 'rm -f main_override.tf' EXIT + cat < main_override.tf module "braintrust-data-plane" { source = "../../" } EOF + terraform init -upgrade + terraform validate + ) +} + +echo "Validating module" terraform init -upgrade terraform validate + +validate_example "examples/braintrust-data-plane" "production example" +# External-EKS leaves brainstore_license_key unset (null default). `validate` +# catches syntax/type/reference mistakes on that wiring; it does NOT evaluate +# locals, so it cannot catch coalesce-on-null style bugs. Plan-mode coverage +# for that is tracked in the follow-up terraform test PR. +validate_example "examples/braintrust-data-plane-external-eks-quarantine" "external-EKS example (license key unset)" """ [tasks.validate-tofu] description = "Validate the Terraform module and example code using OpenTofu" run = """ #!/usr/bin/env bash -set -e -echo "Validating module with OpenTofu" -tofu init -upgrade -tofu validate +set -euo pipefail -echo "Validating example code with OpenTofu" -cd examples/braintrust-data-plane -# Override the module source to point to the local module. -# '*_override.tf' is a lesser known native terraform feature -trap 'rm -f main_override.tf' EXIT -cat < main_override.tf +validate_example() { + local example_dir="$1" + local label="$2" + echo "Validating ${label} with OpenTofu" + ( + cd "${example_dir}" + # Override the module source to point to the local module. + # '*_override.tf' is a lesser known native terraform feature + trap 'rm -f main_override.tf' EXIT + cat < main_override.tf module "braintrust-data-plane" { source = "../../" } EOF + tofu init -upgrade + tofu validate + ) +} + +echo "Validating module with OpenTofu" tofu init -upgrade tofu validate + +validate_example "examples/braintrust-data-plane" "production example" +# External-EKS leaves brainstore_license_key unset (null default). `validate` +# catches syntax/type/reference mistakes on that wiring; it does NOT evaluate +# locals, so it cannot catch coalesce-on-null style bugs. Plan-mode coverage +# for that is tracked in the follow-up terraform test PR. +validate_example "examples/braintrust-data-plane-external-eks-quarantine" "external-EKS example (license key unset)" """ diff --git a/modules/api-ecs/braintrust-api-background.tf b/modules/api-ecs/braintrust-api-background.tf index 6002ea59..6185ff1d 100644 --- a/modules/api-ecs/braintrust-api-background.tf +++ b/modules/api-ecs/braintrust-api-background.tf @@ -18,6 +18,9 @@ resource "aws_ecs_task_definition" "braintrust_api_background" { container_definitions = local.api_container_definitions[local.braintrust_api_background_name] + # Ensure GetSecretValue is granted before a revision that references secrets is registered. + depends_on = [aws_iam_role_policy.task_execution_secrets] + tags = merge({ Name = "${var.deployment_name}-${local.braintrust_api_background_name}" }, local.common_tags) @@ -69,7 +72,11 @@ resource "aws_ecs_service" "braintrust_api_background" { # Path rules associate this target group with the ALB, which ECS requires # before CreateService will attach the service. - depends_on = [aws_lb_listener_rule.alb_path_routes] + # Also wait for task-exec secret IAM before rolling tasks that resolve secrets. + depends_on = [ + aws_lb_listener_rule.alb_path_routes, + aws_iam_role_policy.task_execution_secrets, + ] lifecycle { create_before_destroy = false diff --git a/modules/api-ecs/braintrust-api-ingest.tf b/modules/api-ecs/braintrust-api-ingest.tf index 4a509f42..ae9689ef 100644 --- a/modules/api-ecs/braintrust-api-ingest.tf +++ b/modules/api-ecs/braintrust-api-ingest.tf @@ -18,6 +18,9 @@ resource "aws_ecs_task_definition" "braintrust_api_ingest" { container_definitions = local.api_container_definitions[local.braintrust_api_ingest_name] + # Ensure GetSecretValue is granted before a revision that references secrets is registered. + depends_on = [aws_iam_role_policy.task_execution_secrets] + tags = merge({ Name = "${var.deployment_name}-${local.braintrust_api_ingest_name}" }, local.common_tags) @@ -69,7 +72,11 @@ resource "aws_ecs_service" "braintrust_api_ingest" { # Path rules associate this target group with the ALB, which ECS requires # before CreateService will attach the service. - depends_on = [aws_lb_listener_rule.alb_path_routes] + # Also wait for task-exec secret IAM before rolling tasks that resolve secrets. + depends_on = [ + aws_lb_listener_rule.alb_path_routes, + aws_iam_role_policy.task_execution_secrets, + ] lifecycle { create_before_destroy = false diff --git a/modules/api-ecs/braintrust-api.tf b/modules/api-ecs/braintrust-api.tf index c7743347..dbf69d70 100644 --- a/modules/api-ecs/braintrust-api.tf +++ b/modules/api-ecs/braintrust-api.tf @@ -18,6 +18,9 @@ resource "aws_ecs_task_definition" "braintrust_api" { container_definitions = local.api_container_definitions[local.braintrust_api_name] + # Ensure GetSecretValue is granted before a revision that references secrets is registered. + depends_on = [aws_iam_role_policy.task_execution_secrets] + tags = merge({ Name = "${var.deployment_name}-${local.braintrust_api_name}" }, local.common_tags) @@ -69,7 +72,11 @@ resource "aws_ecs_service" "braintrust_api" { # The listener default action associates this target group with the ALB, # which ECS requires before CreateService will attach the service. - depends_on = [aws_lb_listener.api_ecs_http] + # Also wait for task-exec secret IAM before rolling tasks that resolve secrets. + depends_on = [ + aws_lb_listener.api_ecs_http, + aws_iam_role_policy.task_execution_secrets, + ] lifecycle { create_before_destroy = false diff --git a/modules/api-ecs/locals.tf b/modules/api-ecs/locals.tf index a3ec31b1..c1d4fffe 100644 --- a/modules/api-ecs/locals.tf +++ b/modules/api-ecs/locals.tf @@ -1,6 +1,11 @@ locals { - api_version_tag = var.api_version_override != null ? var.api_version_override : jsondecode(file("${path.module}/VERSIONS.json"))["api"] - observability_enabled = var.internal_observability_api_key_secret_arn != "" + api_version_tag = var.api_version_override != null ? var.api_version_override : jsondecode(file("${path.module}/VERSIONS.json"))["api"] + # Use the plan-known enable flag (not the computed secret ARN) so container/IAM shape stays known during plan. + observability_enabled = var.internal_observability_enabled + # Pin the secret version in valueFrom so rotating the key revises the task definition and rolls the service. + # Format: arn:...:secret:name:json-key:version-stage:version-id + # Empty json-key = full secret string; empty stage pins by version-id only. + observability_api_key_value_from = "${var.internal_observability_api_key_secret_arn}:::${var.internal_observability_api_key_secret_version}" common_tags = merge({ BraintrustDeploymentName = var.deployment_name @@ -201,7 +206,7 @@ locals { secretOptions = [ { name = "apikey" - valueFrom = var.internal_observability_api_key_secret_arn + valueFrom = local.observability_api_key_value_from } ] }) : jsonencode({ @@ -297,7 +302,7 @@ locals { secrets = [ { name = "DD_API_KEY" - valueFrom = var.internal_observability_api_key_secret_arn + valueFrom = local.observability_api_key_value_from } ] healthCheck = { diff --git a/modules/api-ecs/variables.tf b/modules/api-ecs/variables.tf index 81cb2d0f..f7cc5d6a 100644 --- a/modules/api-ecs/variables.tf +++ b/modules/api-ecs/variables.tf @@ -536,6 +536,18 @@ variable "internal_observability_api_key_secret_arn" { default = "" } +variable "internal_observability_api_key_secret_version" { + type = string + description = "Version ID of the observability API key secret. Included in the task definition so rotating the key forces a new revision and service rollout." + default = "" +} + +variable "internal_observability_enabled" { + type = bool + description = "Whether to enable internal Datadog observability for API ECS. Must be a plan-known boolean (do not derive from the secret ARN)." + default = false +} + variable "internal_observability_region" { type = string description = "Datadog region suffix (e.g. us5) used to build DD_SITE." diff --git a/modules/brainstore-ec2/main-fast-reader.tf b/modules/brainstore-ec2/main-fast-reader.tf index 3efe5b2e..2699aef0 100644 --- a/modules/brainstore-ec2/main-fast-reader.tf +++ b/modules/brainstore-ec2/main-fast-reader.tf @@ -36,35 +36,37 @@ resource "aws_launch_template" "brainstore_fast_reader" { } user_data = base64gzip(templatefile("${path.module}/templates/user_data.sh.tpl", { - aws_region = data.aws_region.current.region - deployment_name = var.deployment_name - database_secret_arn = var.database_secret_arn - database_host = var.database_host - database_port = var.database_port - redis_scheme = var.use_redis_replication_group ? "rediss" : "redis" - redis_host = var.redis_host - redis_port = var.redis_port - brainstore_port = var.port - brainstore_s3_bucket = local.brainstore_s3_bucket_id - lambda_responses_bucket_id = local.lambda_responses_bucket_id - code_bundle_bucket_id = local.code_bundle_bucket_id - brainstore_locks_s3_path = trimprefix(var.locks_s3_path, "/") - brainstore_license_key = var.license_key - brainstore_version_override = var.version_override == null ? "" : var.version_override - brainstore_release_version = local.brainstore_release_version - monitoring_telemetry = var.monitoring_telemetry - is_dedicated_reader_node = "true" - is_dedicated_writer_node = "false" - extra_env_vars = var.extra_env_vars_fast_reader - internal_observability_api_key = var.internal_observability_api_key - internal_observability_env_name = var.internal_observability_env_name - internal_observability_region = var.internal_observability_region - service_token_secret_arn = var.service_token_secret_arn - custom_post_install_script = var.custom_post_install_script - brainstore_cache_file_size = local.brainstore_fast_reader_cache_file_size - skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects - brainstore_enable_export = var.brainstore_enable_export - ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter + aws_region = data.aws_region.current.region + deployment_name = var.deployment_name + database_secret_arn = var.database_secret_arn + database_host = var.database_host + database_port = var.database_port + redis_scheme = var.use_redis_replication_group ? "rediss" : "redis" + redis_host = var.redis_host + redis_port = var.redis_port + brainstore_port = var.port + brainstore_s3_bucket = local.brainstore_s3_bucket_id + lambda_responses_bucket_id = local.lambda_responses_bucket_id + code_bundle_bucket_id = local.code_bundle_bucket_id + brainstore_locks_s3_path = trimprefix(var.locks_s3_path, "/") + license_key_secret_arn = var.license_key_secret_arn + license_key_secret_version = var.license_key_secret_version + brainstore_version_override = var.version_override == null ? "" : var.version_override + brainstore_release_version = local.brainstore_release_version + monitoring_telemetry = var.monitoring_telemetry + is_dedicated_reader_node = "true" + is_dedicated_writer_node = "false" + extra_env_vars = var.extra_env_vars_fast_reader + internal_observability_api_key_secret_arn = var.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = var.internal_observability_api_key_secret_version + internal_observability_env_name = var.internal_observability_env_name + internal_observability_region = var.internal_observability_region + service_token_secret_arn = var.service_token_secret_arn + custom_post_install_script = var.custom_post_install_script + brainstore_cache_file_size = local.brainstore_fast_reader_cache_file_size + skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects + brainstore_enable_export = var.brainstore_enable_export + ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter })) tags = merge({ diff --git a/modules/brainstore-ec2/main-writer.tf b/modules/brainstore-ec2/main-writer.tf index eb92bdc8..bea5728d 100644 --- a/modules/brainstore-ec2/main-writer.tf +++ b/modules/brainstore-ec2/main-writer.tf @@ -36,35 +36,37 @@ resource "aws_launch_template" "brainstore_writer" { } user_data = base64gzip(templatefile("${path.module}/templates/user_data.sh.tpl", { - aws_region = data.aws_region.current.region - deployment_name = var.deployment_name - database_secret_arn = var.database_secret_arn - database_host = var.database_host - database_port = var.database_port - redis_scheme = var.use_redis_replication_group ? "rediss" : "redis" - redis_host = var.redis_host - redis_port = var.redis_port - brainstore_port = var.port - brainstore_s3_bucket = local.brainstore_s3_bucket_id - lambda_responses_bucket_id = local.lambda_responses_bucket_id - code_bundle_bucket_id = local.code_bundle_bucket_id - brainstore_locks_s3_path = trimprefix(var.locks_s3_path, "/") - brainstore_license_key = var.license_key - brainstore_version_override = var.version_override == null ? "" : var.version_override - brainstore_release_version = local.brainstore_release_version - monitoring_telemetry = var.monitoring_telemetry - is_dedicated_reader_node = "false" - is_dedicated_writer_node = "true" - extra_env_vars = var.extra_env_vars_writer - internal_observability_api_key = var.internal_observability_api_key - internal_observability_env_name = var.internal_observability_env_name - internal_observability_region = var.internal_observability_region - service_token_secret_arn = var.service_token_secret_arn - custom_post_install_script = var.custom_post_install_script - brainstore_cache_file_size = local.brainstore_writer_cache_file_size - skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects - brainstore_enable_export = var.brainstore_enable_export - ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter + aws_region = data.aws_region.current.region + deployment_name = var.deployment_name + database_secret_arn = var.database_secret_arn + database_host = var.database_host + database_port = var.database_port + redis_scheme = var.use_redis_replication_group ? "rediss" : "redis" + redis_host = var.redis_host + redis_port = var.redis_port + brainstore_port = var.port + brainstore_s3_bucket = local.brainstore_s3_bucket_id + lambda_responses_bucket_id = local.lambda_responses_bucket_id + code_bundle_bucket_id = local.code_bundle_bucket_id + brainstore_locks_s3_path = trimprefix(var.locks_s3_path, "/") + license_key_secret_arn = var.license_key_secret_arn + license_key_secret_version = var.license_key_secret_version + brainstore_version_override = var.version_override == null ? "" : var.version_override + brainstore_release_version = local.brainstore_release_version + monitoring_telemetry = var.monitoring_telemetry + is_dedicated_reader_node = "false" + is_dedicated_writer_node = "true" + extra_env_vars = var.extra_env_vars_writer + internal_observability_api_key_secret_arn = var.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = var.internal_observability_api_key_secret_version + internal_observability_env_name = var.internal_observability_env_name + internal_observability_region = var.internal_observability_region + service_token_secret_arn = var.service_token_secret_arn + custom_post_install_script = var.custom_post_install_script + brainstore_cache_file_size = local.brainstore_writer_cache_file_size + skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects + brainstore_enable_export = var.brainstore_enable_export + ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter })) tags = merge({ diff --git a/modules/brainstore-ec2/main.tf b/modules/brainstore-ec2/main.tf index 06834b89..3964f3cf 100644 --- a/modules/brainstore-ec2/main.tf +++ b/modules/brainstore-ec2/main.tf @@ -70,23 +70,25 @@ resource "aws_launch_template" "brainstore" { lambda_responses_bucket_id = local.lambda_responses_bucket_id code_bundle_bucket_id = local.code_bundle_bucket_id brainstore_locks_s3_path = trimprefix(var.locks_s3_path, "/") - brainstore_license_key = var.license_key + license_key_secret_arn = var.license_key_secret_arn + license_key_secret_version = var.license_key_secret_version brainstore_version_override = var.version_override == null ? "" : var.version_override brainstore_release_version = local.brainstore_release_version monitoring_telemetry = var.monitoring_telemetry # Important note: if there are no dedicated writer nodes, this node serves as a read/writer node - is_dedicated_reader_node = local.has_writer_nodes ? "true" : "false" - is_dedicated_writer_node = "false" - extra_env_vars = var.extra_env_vars - internal_observability_api_key = var.internal_observability_api_key - internal_observability_env_name = var.internal_observability_env_name - internal_observability_region = var.internal_observability_region - service_token_secret_arn = var.service_token_secret_arn - custom_post_install_script = var.custom_post_install_script - brainstore_cache_file_size = local.brainstore_cache_file_size - skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects - brainstore_enable_export = var.brainstore_enable_export - ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter + is_dedicated_reader_node = local.has_writer_nodes ? "true" : "false" + is_dedicated_writer_node = "false" + extra_env_vars = var.extra_env_vars + internal_observability_api_key_secret_arn = var.internal_observability_api_key_secret_arn + internal_observability_api_key_secret_version = var.internal_observability_api_key_secret_version + internal_observability_env_name = var.internal_observability_env_name + internal_observability_region = var.internal_observability_region + service_token_secret_arn = var.service_token_secret_arn + custom_post_install_script = var.custom_post_install_script + brainstore_cache_file_size = local.brainstore_cache_file_size + skip_pg_for_brainstore_objects = var.skip_pg_for_brainstore_objects + brainstore_enable_export = var.brainstore_enable_export + ai_proxy_url_ssm_parameter = var.ai_proxy_url_ssm_parameter })) tags = merge({ diff --git a/modules/brainstore-ec2/templates/user_data.sh.tpl b/modules/brainstore-ec2/templates/user_data.sh.tpl index a8e514d5..629204f9 100644 --- a/modules/brainstore-ec2/templates/user_data.sh.tpl +++ b/modules/brainstore-ec2/templates/user_data.sh.tpl @@ -133,6 +133,13 @@ if ! SERVICE_TOKEN_SECRET_KEY=$(aws secretsmanager get-secret-value --secret-id exit 1 fi +# license_key_secret_version is baked into user_data so rotating the secret +# changes the launch template and triggers a rolling instance refresh. +if ! BRAINSTORE_LICENSE_KEY=$(aws secretsmanager get-secret-value --secret-id ${license_key_secret_arn} --version-id ${license_key_secret_version} --query SecretString --output text); then + echo "Failed to retrieve BRAINSTORE_LICENSE_KEY from Secrets Manager. Exiting with failure." + exit 1 +fi + cat < /etc/brainstore.env # WARNING: Do NOT use quotes around values here. They get passed as literals by docker. BRAINSTORE_VERBOSE=1 @@ -145,7 +152,7 @@ BRAINSTORE_WAL_URI=postgres://$DB_USERNAME:$DB_PASSWORD@${database_host}:${datab BRAINSTORE_CACHE_DIR=/mnt/tmp/brainstore BRAINSTORE_RESPONSE_CACHE_URI=s3://${lambda_responses_bucket_id}/brainstore-cache BRAINSTORE_CODE_BUNDLE_URI=s3://${code_bundle_bucket_id} -BRAINSTORE_LICENSE_KEY=${brainstore_license_key} +BRAINSTORE_LICENSE_KEY=$BRAINSTORE_LICENSE_KEY BRAINSTORE_READER_ONLY_MODE=${is_dedicated_reader_node} BRAINSTORE_CONTROL_PLANE_TELEMETRY=${monitoring_telemetry} SERVICE_TOKEN_SECRET_KEY=$SERVICE_TOKEN_SECRET_KEY @@ -196,12 +203,19 @@ if [ "${is_dedicated_writer_node}" = "true" ]; then echo '0 * * * * root sleep $(shuf -i 0-1800 -n 1) && /usr/bin/docker restart brainstore >> /var/log/brainstore-restart.log 2>&1' > /etc/cron.d/restart-brainstore fi -if [ -n "${internal_observability_api_key}" ]; then +if [ -n "${internal_observability_api_key_secret_arn}" ]; then + # internal_observability_api_key_secret_version is baked into user_data so + # rotating the secret changes the launch template and triggers a rolling + # instance refresh. + if ! DD_API_KEY=$(aws secretsmanager get-secret-value --secret-id ${internal_observability_api_key_secret_arn} --version-id ${internal_observability_api_key_secret_version} --query SecretString --output text); then + echo "Failed to retrieve DD_API_KEY from Secrets Manager. Exiting with failure." + exit 1 + fi if [ -n "${internal_observability_env_name}" ]; then export DD_ENV="${internal_observability_env_name}" fi # Install Datadog Agent - export DD_API_KEY="${internal_observability_api_key}" + export DD_API_KEY export DD_SITE="${internal_observability_region}.datadoghq.com" export DD_APM_INSTRUMENTATION_ENABLED=host export DD_APM_INSTRUMENTATION_LIBRARIES=java:1,python:3,js:5,php:1,dotnet:3 diff --git a/modules/brainstore-ec2/variables.tf b/modules/brainstore-ec2/variables.tf index 31ca3c1d..d3c1f33d 100644 --- a/modules/brainstore-ec2/variables.tf +++ b/modules/brainstore-ec2/variables.tf @@ -9,15 +9,20 @@ variable "instance_type" { default = "c8gd.4xlarge" } -variable "license_key" { +variable "license_key_secret_arn" { type = string - description = "The license key for the Brainstore" + description = "ARN of the Secrets Manager secret containing the Brainstore license key." validation { - condition = var.license_key != null && length(var.license_key) > 0 - error_message = "The license key cannot be empty." + condition = var.license_key_secret_arn != null && length(var.license_key_secret_arn) > 0 + error_message = "The license key secret ARN cannot be empty." } } +variable "license_key_secret_version" { + type = string + description = "Version ID of the license key secret. Baked into user_data so rotating the key forces a rolling instance refresh." +} + variable "instance_count" { type = number description = "The number of reader instances to create" @@ -148,9 +153,15 @@ variable "monitoring_telemetry" { } } -variable "internal_observability_api_key" { +variable "internal_observability_api_key_secret_arn" { type = string - description = "Support for internal observability agent. Do not set this unless instructed by support." + description = "ARN of the Secrets Manager secret containing the Datadog API key. Empty disables the Datadog agent on Brainstore instances." + default = "" +} + +variable "internal_observability_api_key_secret_version" { + type = string + description = "Version ID of the observability API key secret. Baked into user_data so rotating the key forces a rolling instance refresh." default = "" } diff --git a/modules/gateway-ecs/main.tf b/modules/gateway-ecs/main.tf index 3a255abc..1d9020dc 100644 --- a/modules/gateway-ecs/main.tf +++ b/modules/gateway-ecs/main.tf @@ -3,13 +3,17 @@ locals { BraintrustDeploymentName = var.deployment_name }, var.custom_tags) - container_name = "gateway" - container_port = 8080 - observability_enabled = var.internal_observability_enabled - gateway_version_tag = element(reverse(split(":", var.container_image)), 0) - unsafe_url_request_mode = var.unsafe_url_request_mode == null ? "" : trimspace(var.unsafe_url_request_mode) - url_security_dns_servers = var.url_security_dns_servers == null ? "" : trimspace(var.url_security_dns_servers) - url_security_allow_cidrs = var.url_security_allow_cidrs == null ? "" : trimspace(var.url_security_allow_cidrs) + container_name = "gateway" + container_port = 8080 + observability_enabled = var.internal_observability_enabled + # Pin the secret version in valueFrom so rotating the key revises the task definition and rolls the service. + # Format: arn:...:secret:name:json-key:version-stage:version-id + # Empty json-key = full secret string; empty stage pins by version-id only. + observability_api_key_value_from = "${var.internal_observability_api_key_secret_arn}:::${var.internal_observability_api_key_secret_version}" + gateway_version_tag = element(reverse(split(":", var.container_image)), 0) + unsafe_url_request_mode = var.unsafe_url_request_mode == null ? "" : trimspace(var.unsafe_url_request_mode) + url_security_dns_servers = var.url_security_dns_servers == null ? "" : trimspace(var.url_security_dns_servers) + url_security_allow_cidrs = var.url_security_allow_cidrs == null ? "" : trimspace(var.url_security_allow_cidrs) url_security_env_vars = merge( local.unsafe_url_request_mode != "" ? { BRAINTRUST_UNSAFE_URL_REQUEST_MODE = local.unsafe_url_request_mode @@ -42,10 +46,17 @@ locals { DD_TRACE_DISABLED_PLUGINS = var.internal_observability_trace_disabled_plugins } : {}, ) - plain_license_env_var = var.brainstore_license_key == null ? {} : { - BRAINSTORE_LICENSE_KEY = var.brainstore_license_key - } - merged_env_vars = merge(local.base_env_vars, local.plain_license_env_var, var.extra_env_vars) + # Use the plan-known enable flag (not the computed secret ARN) so count/for_each stay known during plan. + license_key_enabled = var.brainstore_license_key_enabled + merged_env_vars = merge(local.base_env_vars, var.extra_env_vars) + # Pin the secret version in valueFrom so rotating the key revises the task definition and rolls the service. + # Format: arn:...:secret:name:json-key:version-stage:version-id (empty json-key = full secret string). + gateway_secrets = local.license_key_enabled ? [ + { + name = "BRAINSTORE_LICENSE_KEY" + valueFrom = "${var.brainstore_license_key_secret_arn}:::${var.brainstore_license_key_secret_version}" + } + ] : [] gateway_container_definition = { name = local.container_name @@ -64,6 +75,7 @@ locals { value = local.merged_env_vars[key] } ] + secrets = local.gateway_secrets dependsOn = [ for dep in [ { @@ -134,7 +146,7 @@ locals { secrets = [ { name = "DD_API_KEY" - valueFrom = var.internal_observability_api_key_secret_arn + valueFrom = local.observability_api_key_value_from } ] healthCheck = { @@ -244,10 +256,10 @@ resource "aws_iam_role_policy_attachment" "task_execution_default" { policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } -resource "aws_iam_role_policy" "task_execution_observability_secrets" { - count = local.observability_enabled ? 1 : 0 +resource "aws_iam_role_policy" "task_execution_secrets" { + count = local.observability_enabled || local.license_key_enabled ? 1 : 0 - name = "${var.deployment_name}-gateway-task-exec-observability-secrets" + name = "${var.deployment_name}-gateway-task-exec-secrets" role = aws_iam_role.task_execution.id policy = jsonencode({ @@ -258,7 +270,10 @@ resource "aws_iam_role_policy" "task_execution_observability_secrets" { Action = [ "secretsmanager:GetSecretValue", ] - Resource = var.internal_observability_api_key_secret_arn + Resource = compact([ + local.observability_enabled ? var.internal_observability_api_key_secret_arn : "", + local.license_key_enabled ? var.brainstore_license_key_secret_arn : "", + ]) }, { Effect = "Allow" @@ -323,6 +338,9 @@ resource "aws_ecs_task_definition" "gateway" { container_definitions = jsonencode(concat([local.gateway_container_definition], local.observability_sidecars)) + # Ensure GetSecretValue is granted before a revision that references secrets is registered. + depends_on = [aws_iam_role_policy.task_execution_secrets] + tags = merge({ Name = "${var.deployment_name}-gateway" }, local.common_tags) @@ -373,7 +391,12 @@ resource "aws_ecs_service" "gateway" { container_port = local.container_port } - depends_on = [terraform_data.gateway_http_listener] + # Wait for listener readiness and for task-exec secret IAM before rolling tasks + # that resolve BRAINSTORE_LICENSE_KEY / DD_API_KEY from Secrets Manager. + depends_on = [ + terraform_data.gateway_http_listener, + aws_iam_role_policy.task_execution_secrets, + ] lifecycle { ignore_changes = [desired_count] diff --git a/modules/gateway-ecs/variables.tf b/modules/gateway-ecs/variables.tf index 1a189589..37d79dbf 100644 --- a/modules/gateway-ecs/variables.tf +++ b/modules/gateway-ecs/variables.tf @@ -138,6 +138,12 @@ variable "internal_observability_api_key_secret_arn" { default = "" } +variable "internal_observability_api_key_secret_version" { + type = string + description = "Version ID of the observability API key secret. Included in the task definition so rotating the key forces a new revision and service rollout." + default = "" +} + variable "internal_observability_enabled" { type = bool description = "Whether to enable internal Datadog observability for gateway ECS." @@ -169,7 +175,7 @@ variable "extra_env_vars" { validation { condition = !contains(keys(var.extra_env_vars), "BRAINSTORE_LICENSE_KEY") - error_message = "Do not set BRAINSTORE_LICENSE_KEY in extra_env_vars; use brainstore_license_key." + error_message = "Do not set BRAINSTORE_LICENSE_KEY in extra_env_vars; use brainstore_license_key_secret_arn." } } @@ -239,10 +245,22 @@ variable "custom_tags" { default = {} } -variable "brainstore_license_key" { +variable "brainstore_license_key_enabled" { + type = bool + description = "Whether to inject BRAINSTORE_LICENSE_KEY from Secrets Manager. Must be a plan-known boolean (do not derive from the secret ARN)." + default = false +} + +variable "brainstore_license_key_secret_arn" { type = string - description = "License key for the Brainstore instance. Used for telemetry authorization." - default = null + description = "ARN of the Secrets Manager secret containing the Brainstore license key." + default = "" +} + +variable "brainstore_license_key_secret_version" { + type = string + description = "Version ID of the license key secret. Included in the task definition so rotating the key forces a new revision and service rollout." + default = "" } variable "cpu_architecture" { diff --git a/modules/ingress/cloudfront.tf b/modules/ingress/cloudfront.tf index 3744e731..ec2197bf 100644 --- a/modules/ingress/cloudfront.tf +++ b/modules/ingress/cloudfront.tf @@ -36,12 +36,12 @@ locals { : local.cloudfront_AllViewerExceptHostHeader ) cloudfront_origin_request_policy_for_origin = { - (local.cloudfront_ApiEcsOrigin) = local.cloudfront_ecs_origin_request_policy_id - (local.cloudfront_APIGatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader - (local.cloudfront_AIProxyOrigin) = local.cloudfront_AllViewerExceptHostHeader - (local.cloudfront_CloudflareProxy) = local.cloudfront_AllViewerExceptHostHeader - (local.cloudfront_GatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader - (local.cloudfront_PrivateGatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader + (local.cloudfront_ApiEcsOrigin) = local.cloudfront_ecs_origin_request_policy_id + (local.cloudfront_APIGatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader + (local.cloudfront_AIProxyOrigin) = local.cloudfront_AllViewerExceptHostHeader + (local.cloudfront_CloudflareProxy) = local.cloudfront_AllViewerExceptHostHeader + (local.cloudfront_GatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader + (local.cloudfront_PrivateGatewayOrigin) = local.cloudfront_AllViewerExceptHostHeader } } diff --git a/modules/services-common/iam-brainstore.tf b/modules/services-common/iam-brainstore.tf index 18303d0e..7f2457e1 100644 --- a/modules/services-common/iam-brainstore.tf +++ b/modules/services-common/iam-brainstore.tf @@ -123,10 +123,12 @@ resource "aws_iam_role_policy" "brainstore_secrets_access" { { Effect = "Allow" Action = "secretsmanager:GetSecretValue" - Resource = [ + Resource = compact([ var.database_secret_arn, - aws_secretsmanager_secret.function_tools_secret.arn - ] + aws_secretsmanager_secret.function_tools_secret.arn, + var.brainstore_license_key_secret_arn, + var.internal_observability_api_key_secret_arn, + ]) } ] }) diff --git a/modules/services-common/variables.tf b/modules/services-common/variables.tf index 5363d3b8..b07fe55d 100644 --- a/modules/services-common/variables.tf +++ b/modules/services-common/variables.tf @@ -23,6 +23,18 @@ variable "database_secret_arn" { description = "The ARN of the secret containing database credentials" } +variable "brainstore_license_key_secret_arn" { + type = string + description = "ARN of the Secrets Manager secret containing the Brainstore license key. Empty when unused." + default = "" +} + +variable "internal_observability_api_key_secret_arn" { + type = string + description = "ARN of the Secrets Manager secret containing the Datadog API key. Empty when unused." + default = "" +} + variable "permissions_boundary_arn" { type = string description = "ARN of the IAM permissions boundary to apply to all IAM roles created by this module" diff --git a/moved_state.tf b/moved_state.tf index 034abf56..13779782 100644 --- a/moved_state.tf +++ b/moved_state.tf @@ -295,3 +295,10 @@ moved { from = module.services_common.aws_vpc_security_group_ingress_rule.gateway_alb_from_cloudfront_vpc_origin[0] to = module.gateway_alb[0].aws_vpc_security_group_ingress_rule.gateway_alb_from_cloudfront_vpc_origin[0] } + +# Gateway task-exec secrets policy generalized beyond observability-only. +# No-op when observability was never enabled (resource absent from state). +moved { + from = module.gateway_ecs[0].aws_iam_role_policy.task_execution_observability_secrets[0] + to = module.gateway_ecs[0].aws_iam_role_policy.task_execution_secrets[0] +} diff --git a/secrets.tf b/secrets.tf index a594fb8e..0f4dbda7 100644 --- a/secrets.tf +++ b/secrets.tf @@ -17,3 +17,23 @@ resource "aws_secretsmanager_secret_version" "internal_observability_api_key" { secret_id = aws_secretsmanager_secret.internal_observability_api_key[0].id secret_string = var.internal_observability_api_key } + +resource "aws_secretsmanager_secret" "brainstore_license_key" { + count = local.create_brainstore_license_secret ? 1 : 0 + + name = "${var.deployment_name}/brainstore/license-key" + recovery_window_in_days = 0 + kms_key_id = local.kms_key_arn + + tags = merge({ + Name = "${var.deployment_name}-brainstore-license-key" + BraintrustDeploymentName = var.deployment_name + }, local.all_custom_tags) +} + +resource "aws_secretsmanager_secret_version" "brainstore_license_key" { + count = local.create_brainstore_license_secret ? 1 : 0 + + secret_id = aws_secretsmanager_secret.brainstore_license_key[0].id + secret_string = var.brainstore_license_key +} diff --git a/variables.tf b/variables.tf index a74a32df..f2008b56 100644 --- a/variables.tf +++ b/variables.tf @@ -1111,6 +1111,7 @@ variable "brainstore_license_key" { type = string description = "The license key for the Brainstore instance" default = null + sensitive = true } variable "brainstore_version_override" { @@ -1261,6 +1262,7 @@ variable "internal_observability_api_key" { type = string description = "Support for internal observability agent. Do not set this unless instructed by support." default = "" + sensitive = true } variable "internal_observability_env_name" {