Files
Robert Fairburn e2681aea79 Add configurable Fleet private key delivery, optional command override, and caller-managed private key secret ARN support (#205)
## Summary

  This PR extends the Fleet ECS modules to support:

  - configurable Fleet server private key delivery via ecs or iam
  - optional fleet_config.command overrides for Fleet task definitions
- caller-managed Fleet server private key secrets via
fleet_config.private_key_secret_arn

It also updates the external vuln scans addon to follow the same
private-key delivery and command behavior, and refreshes the generated
module documentation.

  ## What changed

- Added fleet_config.private_key_delivery_method with supported values:
- ecs (default): inject the private key as FLEET_SERVER_PRIVATE_KEY
      - iam: inject the secret ARN as FLEET_SERVER_PRIVATE_KEY_ARN
  - Added validation so only ecs or iam are accepted
- Added fleet_config.command support so callers can override the Fleet
container command when needed
- Added fleet_config.private_key_secret_arn support so callers can
supply an existing Secrets Manager secret instead of having this module
create and populate one
- Updated IAM policy wiring so private-key secret access and private-key
KMS decrypt permissions are granted to the correct role based on the
selected delivery method
- Updated module outputs so fleet_server_private_key_secret_arn returns
the effective ARN whether the secret is module-managed or caller-managed
- Updated addons/external-vuln-scans to consume the same private-key
delivery mode and optional command behavior from the passed fleet_config
  - Updated byo-ecs documentation and regenerated README files

  ## Behavior

  ### Private key delivery

- ecs mode injects the secret value into the container and uses the ECS
execution role for private-key secret access
- iam mode injects the secret ARN into the container environment and
uses the Fleet task role for private-key secret access

  ### Command override

- When fleet_config.command is unset, the task definition omits command
- When fleet_config.command is set, the provided command is passed
through to the Fleet container
- This behavior now applies consistently to both the main ECS service
and external vuln scans

  ### Caller-managed private key secret

- When fleet_config.private_key_secret_arn is unset, the module creates
and populates the Fleet private key secret
- When fleet_config.private_key_secret_arn is set, the module uses the
provided secret ARN and does not create the secret, secret version, or
generated random value

  ## KMS note for external secrets

If a caller-managed private-key secret uses a CMK, the caller must also
provide the existing private-key KMS ARN input so the correct role
receives decrypt permissions for the selected delivery mode.

  ## Validation

  Validated successfully with:

  - terraform validate -no-color at repo root
  - terraform validate -no-color in byo-vpc
  - terraform validate -no-color in byo-vpc/byo-db
  - terraform validate -no-color in byo-vpc/byo-db/byo-ecs

Also verified that existing module-managed private-key resources plan as
non-destructive state-address moves when transitioning to the new
counted resources:

  - 0 to add
  - 0 to change
  - 0 to destroy
2026-04-06 19:25:17 -05:00

174 lines
6.2 KiB
Terraform

data "aws_region" "current" {}
locals {
environment = [
// specifically overriding disable schedule here because the output of this module sets this to true
// and then we pull in the output of fleet ecs module
for k, v in merge(
var.fleet_config.extra_environment_variables,
{ FLEET_VULNERABILITIES_DISABLE_SCHEDULE = "false" }
) : {
name = k
value = v
}
]
secrets = [
for k, v in merge(var.fleet_config.extra_secrets, {
FLEET_MYSQL_PASSWORD = var.fleet_config.database.password_secret_arn
FLEET_MYSQL_READ_REPLICA_PASSWORD = var.fleet_config.database.password_secret_arn
}, var.fleet_config.private_key_delivery_method == "ecs" ? {
FLEET_SERVER_PRIVATE_KEY = var.fleet_server_private_key_secret_arn
} : {}) : {
name = k
valueFrom = v
}
]
repository_credentials = var.fleet_config.repository_credentials != "" ? {
repositoryCredentials = {
credentialsParameter = var.fleet_config.repository_credentials
}
} : null
}
resource "aws_ecs_service" "fleet" {
name = "${var.fleet_config.service.name}-vuln-processing"
launch_type = "FARGATE"
cluster = var.ecs_cluster
task_definition = aws_ecs_task_definition.vuln-processing.arn
desired_count = 1
deployment_minimum_healthy_percent = 100
deployment_maximum_percent = 200
lifecycle {
ignore_changes = [desired_count]
}
network_configuration {
subnets = var.subnets
security_groups = var.security_groups
}
}
resource "aws_ecs_task_definition" "vuln-processing" {
family = "${var.fleet_config.family}-vuln-processing"
cpu = var.vuln_processing_task_cpu
memory = var.vuln_processing_task_memory
execution_role_arn = var.execution_iam_role_arn
task_role_arn = var.task_role_arn
network_mode = "awsvpc"
pid_mode = var.fleet_config.pid_mode
requires_compatibilities = ["FARGATE"]
container_definitions = jsonencode(concat([
merge(
{
name = "fleet-vuln-processing"
image = var.fleet_config.image
cpu = var.vuln_processing_cpu
memory = var.vuln_processing_memory
essential = true
networkMode = "awsvpc"
secrets = local.secrets
repositoryCredentials = local.repository_credentials
mountPoints = var.fleet_config.mount_points
dependsOn = var.fleet_config.depends_on
ulimits = [
{
name = "nofile"
softLimit = 999999
hardLimit = 999999
}
]
environment = concat([
{
name = "FLEET_MYSQL_USERNAME"
value = var.fleet_config.database.user
},
{
name = "FLEET_MYSQL_DATABASE"
value = var.fleet_config.database.database
},
{
name = "FLEET_MYSQL_ADDRESS"
value = var.fleet_config.database.address
},
{
name = "FLEET_MYSQL_READ_REPLICA_USERNAME"
value = var.fleet_config.database.user
},
{
name = "FLEET_MYSQL_READ_REPLICA_DATABASE"
value = var.fleet_config.database.database
},
{
name = "FLEET_MYSQL_READ_REPLICA_ADDRESS"
value = var.fleet_config.database.rr_address == null ? var.fleet_config.database.address : var.fleet_config.database.rr_address
},
{
name = "FLEET_REDIS_ADDRESS"
value = var.fleet_config.redis.address
},
{
name = "FLEET_REDIS_USE_TLS"
value = tostring(var.fleet_config.redis.use_tls)
},
{
name = "FLEET_SERVER_TLS"
value = tostring(try(var.fleet_config.server_tls_enabled, false))
},
{
name = "FLEET_S3_SOFTWARE_INSTALLERS_BUCKET"
value = var.fleet_s3_software_installers_config.bucket_name
},
{
name = "FLEET_S3_SOFTWARE_INSTALLERS_PREFIX"
value = var.fleet_s3_software_installers_config.s3_object_prefix
}
], var.fleet_config.private_key_delivery_method == "iam" ? [{
name = "FLEET_SERVER_PRIVATE_KEY_ARN"
value = var.fleet_server_private_key_secret_arn
}] : [], local.environment)
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = var.awslogs_config.group
awslogs-region = var.awslogs_config.region == null ? data.aws_region.current.region : var.awslogs_config.region
awslogs-stream-prefix = "${var.awslogs_config.prefix}-vuln-processing"
}
}
},
var.fleet_config.command != null ? {
command = var.fleet_config.command
} : {}
)]
, var.fleet_config.sidecars))
dynamic "volume" {
for_each = var.fleet_config.volumes
content {
name = volume.value.name
host_path = lookup(volume.value, "host_path", null)
dynamic "docker_volume_configuration" {
for_each = lookup(volume.value, "docker_volume_configuration", [])
content {
scope = lookup(docker_volume_configuration.value, "scope", null)
autoprovision = lookup(docker_volume_configuration.value, "autoprovision", null)
driver = lookup(docker_volume_configuration.value, "driver", null)
driver_opts = lookup(docker_volume_configuration.value, "driver_opts", null)
labels = lookup(docker_volume_configuration.value, "labels", null)
}
}
dynamic "efs_volume_configuration" {
for_each = lookup(volume.value, "efs_volume_configuration", [])
content {
file_system_id = lookup(efs_volume_configuration.value, "file_system_id", null)
root_directory = lookup(efs_volume_configuration.value, "root_directory", null)
}
}
}
}
}