Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions modules/runner-config/README.md

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions modules/runner-config/common-config.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Shared control-plane configuration: naming, paths, tags, and normalized values.
locals {
common_tags = var.tags
runner_tags = merge(local.common_tags, var.runner.tags)
lambda_tags = merge(local.common_tags, var.lambda.tags)
observability_log_tags = merge(local.common_tags, var.observability.logs.tags)

ssm_tags = merge(local.common_tags, var.ssm.tags)
ssm_parameter_tags = merge(local.ssm_tags, var.ssm.parameters.tags)
ssm_housekeeper_tags = merge(local.ssm_tags, var.ssm.housekeeper.tags)
ssm_housekeeper_lambda_tags = merge(local.lambda_tags, var.ssm.tags, var.ssm.housekeeper.tags)
ssm_housekeeper_log_tags = merge(local.observability_log_tags, var.ssm.tags, var.ssm.housekeeper.tags)

lambda_role_path = var.lambda.role.path == null ? "/${var.prefix}/" : var.lambda.role.path
runner_role_path = var.runner.iam.path == null ? "/${var.prefix}/" : var.runner.iam.path
packaged_runners_lambda_zip = "${path.module}/../../lambdas/functions/control-plane/runners.zip"
ssm_housekeeper_artifact_s3_selected = (
var.ssm.housekeeper.lambda.artifact.s3 != null
)
ssm_housekeeper_artifact = {
zip = local.ssm_housekeeper_artifact_s3_selected ? null : coalesce(
var.ssm.housekeeper.lambda.artifact.zip,
local.packaged_runners_lambda_zip,
)
s3 = {
bucket = local.ssm_housekeeper_artifact_s3_selected ? var.lambda.artifact.s3.bucket : null
key = try(var.ssm.housekeeper.lambda.artifact.s3.key, null)
object_version = try(var.ssm.housekeeper.lambda.artifact.s3.object_version, null)
}
}
kms_key_id = var.ssm.kms_key_id
token_path = "${var.ssm.paths.root}/${var.ssm.paths.tokens}"
arn_ssm_parameters_path_tokens = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.ssm.paths.root}/${var.ssm.paths.tokens}"
arn_ssm_parameters_path_config = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.ssm.paths.root}/${var.ssm.paths.config}"

parameter_store_tags = jsonencode([
for key, value in local.ssm_parameter_tags : {
Key = key
Value = value
}
])
}

data "aws_caller_identity" "current" {}
37 changes: 37 additions & 0 deletions modules/runner-config/compute-provider.aws.ec2.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module "compute_aws_ec2_trust_policy" {
count = local.provider_key == "aws_ec2" ? 1 : 0
source = "../compute-providers/aws/ec2/trust-policy"

additional_trust_policy_json = var.runner.iam.additional_trust_policy_json
}

module "compute_aws_ec2" {
count = local.provider_key == "aws_ec2" ? 1 : 0
source = "../compute-providers/aws/ec2"

aws_partition = var.aws_partition
aws_region = var.aws_region
prefix = var.prefix
tags = var.tags

config = var.compute_provider.aws.ec2
runner = merge(var.runner, {
iam = merge(var.runner.iam, {
role = local.runner_role
managed_policy_arns = local.common_runner_managed_policy_arns
})
})
github = var.github
ssm = var.ssm
observability = var.observability
}

moved {
from = module.compute_ec2_trust_policy
to = module.compute_aws_ec2_trust_policy
}

moved {
from = module.compute_ec2
to = module.compute_aws_ec2
}
29 changes: 29 additions & 0 deletions modules/runner-config/compute-provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
locals {
compute_providers = {
aws_ec2 = var.compute_provider.aws.ec2
}

discovered_provider_key = one([
for provider_key, provider_config in local.compute_providers : provider_key
if provider_config != null
])
provider_key = var.compute_provider_key != null ? var.compute_provider_key : local.discovered_provider_key

provider_types = {
aws_ec2 = "ec2"
}

provider_type = local.provider_types[local.provider_key]

provider_assume_role_policies = {
aws_ec2 = try(module.compute_aws_ec2_trust_policy[0].assume_role_policy, null)
}

provider_assume_role_policy = local.provider_assume_role_policies[local.provider_key]

provider_contracts = {
aws_ec2 = one(module.compute_aws_ec2[*].provider)
}

provider_contract = local.provider_contracts[local.provider_key]
}
73 changes: 73 additions & 0 deletions modules/runner-config/orchestration-provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
locals {
orchestration_providers = {
for provider_type, provider_config in var.orchestration_provider : provider_type => provider_config
if provider_config != null
}

orchestration_provider_type = one(keys(local.orchestration_providers))

orchestration_provider_enabled = {
webhook = local.orchestration_provider_type == "webhook"
}

orchestration_provider_runner_lifecycle = {
webhook = one(module.orchestration_webhook[*].runner_lifecycle)
}[local.orchestration_provider_type]
}

module "orchestration_webhook" {
source = "../orchestration-providers/webhook"
count = local.orchestration_provider_enabled.webhook ? 1 : 0

aws_partition = var.aws_partition
prefix = var.prefix
tags = var.tags

config = var.orchestration_provider.webhook
runner = var.runner
github = var.github
lambda = {
artifact = var.lambda.artifact
runtime = var.lambda.runtime
architecture = var.lambda.architecture
subnet_ids = var.lambda.subnet_ids
security_group_ids = var.lambda.security_group_ids
tags = var.lambda.tags
role = {
path = local.lambda_role_path
permissions_boundary = var.lambda.role.permissions_boundary
principals = var.lambda.principals
}
}
ssm = {
token_path = local.token_path
token_path_arn = local.arn_ssm_parameters_path_tokens
config_path = "${var.ssm.paths.root}/${var.ssm.paths.config}"
config_path_arn = local.arn_ssm_parameters_path_config
kms_key_id = local.kms_key_id
parameter_store_tags = local.parameter_store_tags
}
observability = var.observability

runner_provider = {
type = local.provider_type
scale_up = {
environment_variables = local.provider_contract.environment_variables.scale_up
iam_policy_json = local.provider_contract.policies.scale_up.iam_policy_json
additional_iam_policy_json = local.provider_contract.policies.scale_up.additional_iam_policy_json
managed_policy = local.provider_contract.policies.scale_up.managed_policy_enabled ? {
arn = local.provider_contract.policies.scale_up.managed_policy_arn
} : null
}
scale_down = {
environment_variables = local.provider_contract.environment_variables.scale_down
iam_policy_json = local.provider_contract.policies.scale_down.iam_policy_json
}
pool = {
environment_variables = local.provider_contract.environment_variables.pool
iam_policy_json = local.provider_contract.policies.pool.iam_policy_json
managed_policy_enabled = local.provider_contract.policies.pool.managed_policy_enabled
managed_policy_arn = local.provider_contract.policies.pool.managed_policy_arn
}
}
}
42 changes: 42 additions & 0 deletions modules/runner-config/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
output "runner" {
description = "Common runner resources. The role is null when an external runner role is used."
value = {
role = one(aws_iam_role.runner[*])
}
}

output "scale_up" {
description = "Scale-up control-plane resources. Null when webhook orchestration is not configured."
value = one(module.orchestration_webhook[*].scale_up)
}

output "scale_down" {
description = "Scale-down control-plane resources. Null when webhook orchestration is not configured."
value = one(module.orchestration_webhook[*].scale_down)
}

output "pool" {
description = "Scheduled pool resources. Null when no pool configuration is supplied."
value = one(module.orchestration_webhook[*].pool)
}

output "orchestration_provider" {
description = "Resources grouped under the selected runner orchestration provider."
value = {
webhook = local.orchestration_provider_enabled.webhook ? {
scale_up = one(module.orchestration_webhook[*].scale_up)
scale_down = one(module.orchestration_webhook[*].scale_down)
pool = one(module.orchestration_webhook[*].pool)
job_retry = one(module.orchestration_webhook[*].job_retry)
} : null
}
}

output "provider" {
description = "Provider-specific resources grouped under the selected provider namespace and type."
value = {
aws = {
ec2 = local.provider_key == "aws_ec2" ? local.provider_contract.resources : null
}
}
}
48 changes: 48 additions & 0 deletions modules/runner-config/runner-role.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
locals {
# Role ownership belongs to the common runner configuration. The selected trust-policy
# submodule supplies the assume-role document, while the full compute provider
# supplies permissions after the role has been resolved.
create_runner_role = var.runner.iam.role == null

runner_role = {
arn = local.create_runner_role ? one(aws_iam_role.runner[*].arn) : var.runner.iam.role.arn
name = local.create_runner_role ? one(aws_iam_role.runner[*].name) : basename(var.runner.iam.role.arn)
managed = local.create_runner_role
}

common_runner_managed_policy_arns = merge(
{
for policy_name, policy_arn in var.runner.iam.managed_policy_arns :
"user-${policy_name}" => policy_arn
},
var.observability.tracing.mode != null ? {
xray = "arn:${var.aws_partition}:iam::aws:policy/AWSXRayDaemonWriteAccess"
} : {},
)

provider_runner_policies = local.provider_contract.policies.runner
}

resource "aws_iam_role" "runner" {
count = local.create_runner_role ? 1 : 0
name = "${substr("${var.prefix}-runner", 0, 54)}-${substr(md5("${var.prefix}-runner"), 0, 8)}"
assume_role_policy = local.provider_assume_role_policy
path = local.runner_role_path
permissions_boundary = var.runner.iam.permissions_boundary
tags = local.runner_tags
}

resource "aws_iam_role_policy" "runner_provider" {
for_each = local.create_runner_role ? local.provider_runner_policies.inline_policies : {}

name = each.value.name
role = aws_iam_role.runner[0].name
policy = each.value.policy_json
}

resource "aws_iam_role_policy_attachment" "runner" {
for_each = local.create_runner_role ? local.provider_runner_policies.managed_policy_arns : {}

role = aws_iam_role.runner[0].name
policy_arn = each.value
}
28 changes: 28 additions & 0 deletions modules/runner-config/runner-ssm-parameters.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Shared runner configuration stored in SSM Parameter Store.
resource "aws_ssm_parameter" "runner_agent_mode" {
name = "${var.ssm.paths.root}/${var.ssm.paths.config}/agent_mode"
type = "String"
value = local.orchestration_provider_runner_lifecycle.ephemeral ? "ephemeral" : "persistent"
tags = local.ssm_parameter_tags
}

resource "aws_ssm_parameter" "disable_default_labels" {
name = "${var.ssm.paths.root}/${var.ssm.paths.config}/disable_default_labels"
type = "String"
value = var.runner.disable_default_labels
tags = local.ssm_parameter_tags
}

resource "aws_ssm_parameter" "jit_config_enabled" {
name = "${var.ssm.paths.root}/${var.ssm.paths.config}/enable_jit_config"
type = "String"
value = local.orchestration_provider_runner_lifecycle.jit_config_enabled
tags = local.ssm_parameter_tags
}

resource "aws_ssm_parameter" "token_path" {
name = "${var.ssm.paths.root}/${var.ssm.paths.config}/token_path"
type = "String"
value = "${var.ssm.paths.root}/${var.ssm.paths.tokens}"
tags = local.ssm_parameter_tags
}
57 changes: 57 additions & 0 deletions modules/runner-config/ssm-housekeeper.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
locals {
ssm_housekeeper_token_path = coalesce(var.ssm.housekeeper.config.tokenPath, local.token_path)
ssm_housekeeper_parameter_path_arn = (
"arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${local.ssm_housekeeper_token_path}*"
)
}

module "ssm_housekeeper" {
source = "./ssm-housekeeper"

config = {
prefix = var.prefix
aws_partition = var.aws_partition
schedule = {
expression = var.ssm.housekeeper.schedule_expression
state = var.ssm.housekeeper.state
}
cleanup = {
token_path = local.ssm_housekeeper_token_path
parameter_path_arn = local.ssm_housekeeper_parameter_path_arn
minimum_days_old = var.ssm.housekeeper.config.minimumDaysOld
dry_run = var.ssm.housekeeper.config.dryRun
}
lambda = {
# The housekeeper resolves only its component-owned selector and never
# inherits the selected orchestration provider's runner-control artifact.
artifact = local.ssm_housekeeper_artifact
runtime = var.lambda.runtime
architecture = var.lambda.architecture
memory_size = var.ssm.housekeeper.lambda.memory_size
timeout = var.ssm.housekeeper.lambda.timeout
vpc = {
subnet_ids = var.lambda.subnet_ids
security_group_ids = var.lambda.security_group_ids
}
role = {
path = local.lambda_role_path
permissions_boundary = var.lambda.role.permissions_boundary
principals = var.lambda.principals
}
}
observability = {
logs = {
level = var.observability.logs.level
retention_in_days = var.observability.logs.retention_in_days
kms_key_id = var.observability.logs.kms_key_id
class = var.observability.logs.class
}
tracing = var.observability.tracing
}
tags = {
resources = local.ssm_housekeeper_tags
lambda = local.ssm_housekeeper_lambda_tags
log_group = local.ssm_housekeeper_log_tags
}
}
}
Loading