Skip to content
Merged
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
2 changes: 2 additions & 0 deletions modules/compute-providers/aws/ec2/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ output "resources" {
output "provider" {
description = "Nested EC2 compute-provider contract consumed by runner-config."
value = {
type = "ec2"
capabilities = { scale_set = local.scale_set_capability }
environment_variables = local.provider_environment_variables
policies = local.provider_policies
resources = local.provider_resources
Expand Down
257 changes: 257 additions & 0 deletions modules/compute-providers/aws/ec2/scale-set.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Provider-owned runtime and IAM fragments for the additive scale-set
# orchestration capability. GitHub credentials, GitHub scope, desired capacity,
# and boot timeout remain orchestration-owned and are not serialized here.
locals {
scale_set_ec2_instance_criteria = merge(
{
instanceTypes = var.config.instance_types
targetCapacityType = var.config.instance_target_capacity_type
instanceAllocationStrategy = var.config.instance_allocation_strategy
},
var.config.instance_type_priorities == null ? {} : {
instanceTypePriorities = var.config.instance_type_priorities
},
var.config.instance_max_spot_price == null ? {} : {
maxSpotPrice = var.config.instance_max_spot_price
},
)

scale_set_runtime_configuration = merge(
{
region = var.aws_region
environment = var.prefix
runnerNamePrefix = var.runner.name_prefix
jitConfigParameterPath = "${var.ssm.paths.root}/${var.ssm.paths.tokens}"
subnets = var.config.subnet_ids
launchTemplateName = aws_launch_template.runner.name
ec2instanceCriteria = local.scale_set_ec2_instance_criteria
onDemandFailoverOnError = var.config.on_demand_failover_for_errors
useDedicatedHost = var.config.use_dedicated_host
ssmParameterTags = [
for key in sort(keys(local.ssm_parameter_tags)) : {
Key = key
Value = local.ssm_parameter_tags[key]
}
]
},
local.ami_id_ssm_external ? {
amiIdSsmParameterName = local.ami_id_ssm_parameter_name
} : {},
)

scale_set_owned_instance_conditions = [
{
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:Application"
values = toset(["github-action-runner"])
},
{
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:created_by"
values = toset(["scale-set-service"])
},
{
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:environment"
values = toset([var.prefix])
},
]

scale_set_owned_request_conditions = [
{
test = "StringEquals"
variable = "aws:RequestTag/ghr:Application"
values = toset(["github-action-runner"])
},
{
test = "StringEquals"
variable = "aws:RequestTag/ghr:created_by"
values = toset(["scale-set-service"])
},
{
test = "StringEquals"
variable = "aws:RequestTag/ghr:environment"
values = toset([var.prefix])
},
]

scale_set_launch_dependency_resources = toset(concat(
[
"arn:${var.aws_partition}:ec2:${var.aws_region}::image/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:*:snapshot/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:dedicated-host/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:network-interface/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:security-group/*",
aws_launch_template.runner.arn,
],
[
for subnet_id in var.config.subnet_ids :
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}"
],
var.config.key_name == null ? [] : [
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:key-pair/${var.config.key_name}",
],
))

scale_set_create_fleet_dependency_resources = toset(concat(
[
"arn:${var.aws_partition}:ec2:${var.aws_region}::image/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:placement-group/*",
aws_launch_template.runner.arn,
],
[
for subnet_id in var.config.subnet_ids :
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:subnet/${subnet_id}"
],
))

scale_set_iam_statements = merge(
{
describe_ec2 = {
actions = toset([
"ec2:DescribeInstances",
"ec2:DescribeLaunchTemplateVersions",
"ec2:DescribeTags",
])
# These EC2 Describe APIs do not support resource-level permissions.
resources = toset(["*"])
conditions = []
}
create_fleet_dependencies = {
actions = toset(["ec2:CreateFleet"])
resources = local.scale_set_create_fleet_dependency_resources
conditions = []
}
create_owned_fleet_capacity = {
actions = toset(["ec2:CreateFleet"])
resources = toset([
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:fleet/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*",
])
conditions = local.scale_set_owned_request_conditions
}
run_instances_dependencies = {
actions = toset(["ec2:RunInstances"])
resources = local.scale_set_launch_dependency_resources
conditions = []
}
run_owned_instances = {
actions = toset(["ec2:RunInstances"])
resources = toset([
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*",
"arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:volume/*",
])
conditions = local.scale_set_owned_request_conditions
}
tag_runners_on_create = {
actions = toset(["ec2:CreateTags"])
resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*/*"])
conditions = [
{
test = "StringEquals"
variable = "ec2:CreateAction"
values = toset(["CreateFleet", "RunInstances"])
},
]
}
update_owned_runner_tags = {
actions = toset(["ec2:CreateTags"])
resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"])
conditions = concat(local.scale_set_owned_instance_conditions, [
{
test = "ForAllValues:StringEquals"
variable = "aws:TagKeys"
values = toset([
"ghr:github_runner_id",
"ghr:runner_name",
"ghr:scale_set_state",
])
},
])
}
terminate_owned_runners = {
actions = toset(["ec2:TerminateInstances"])
resources = toset(["arn:${var.aws_partition}:ec2:${var.aws_region}:${data.aws_caller_identity.current.account_id}:instance/*"])
conditions = local.scale_set_owned_instance_conditions
}
pass_runner_role = {
actions = toset(["iam:PassRole"])
resources = toset([var.runner.iam.role.arn])
conditions = [
{
test = "StringEquals"
variable = "iam:PassedToService"
values = toset(["ec2.amazonaws.com"])
},
]
}
publish_runner_jit_configuration = {
actions = toset([
"ssm:AddTagsToResource",
"ssm:DeleteParameter",
"ssm:PutParameter",
])
resources = toset([
"${local.ssm_parameter_arn_prefix}${var.ssm.paths.root}/${var.ssm.paths.tokens}/*",
])
conditions = []
}
read_ami_parameter = {
actions = toset([
"ssm:GetParameter",
"ssm:GetParameters",
])
resources = toset([
local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn,
])
conditions = []
}
},
local.ami_kms_key_enabled ? {
use_ami_kms_key = {
actions = toset([
"kms:Decrypt",
"kms:DescribeKey",
"kms:ReEncryptFrom",
"kms:ReEncryptTo",
])
resources = toset([local.ami_kms_key_arn])
conditions = []
}
create_ami_kms_grant = {
actions = toset(["kms:CreateGrant"])
resources = toset([local.ami_kms_key_arn])
conditions = [
{
test = "Bool"
variable = "kms:GrantIsForAWSResource"
values = toset(["true"])
},
]
}
} : {},
var.config.create_service_linked_role_spot ? {
create_spot_service_linked_role = {
actions = toset(["iam:CreateServiceLinkedRole"])
resources = toset([
"arn:${var.aws_partition}:iam::${data.aws_caller_identity.current.account_id}:role/aws-service-role/spot.amazonaws.com/AWSServiceRoleForEC2Spot",
])
conditions = [
{
test = "StringEquals"
variable = "iam:AWSServiceName"
values = toset(["spot.amazonaws.com"])
},
]
}
} : {},
)

scale_set_capability = {
configuration_json = jsonencode(local.scale_set_runtime_configuration)
environment_variables = {}
iam_statements = local.scale_set_iam_statements
}
}
29 changes: 29 additions & 0 deletions modules/compute-providers/aws/ec2/tests/provider.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ run "separates_control_plane_contract_from_ec2_resources" {
error_message = "An external AMI SSM parameter must enable the pool managed policy attachment at plan time."
}

assert {
condition = (
contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameter")
&& contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameters")
&& contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.resources, "arn:aws:ssm:eu-west-1:123456789012:parameter/github-runner/ami-id")
)
error_message = "The scale-set compute role must read an external AMI parameter with both single and batched SSM actions."
}

assert {
condition = (
contains(flatten([
Expand Down Expand Up @@ -199,6 +208,26 @@ run "separates_control_plane_contract_from_ec2_resources" {

}

run "includes_managed_ami_read_in_scale_set_contract" {
command = plan

variables {
config = merge(var.config, {
ami = merge(var.config.ami, {
id_ssm_parameter = null
})
})
}

assert {
condition = (
contains(output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.actions, "ssm:GetParameters")
&& output.provider.capabilities.scale_set.iam_statements.read_ami_parameter.resources != toset([])
)
error_message = "The scale-set compute role must read the module-managed AMI parameter."
}
}

run "accepts_partial_typed_compute_options" {
command = plan

Expand Down
10 changes: 6 additions & 4 deletions modules/multi-runner/README.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions modules/multi-runner/config.experimental.effective.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ locals {
})

github = {
enterprise_server = local.normalized_config.github.enterprise_server
user_agent = local.normalized_config.github.user_agent
enterprise_server = local.normalized_config.github.enterprise_server
runner_owner = local.normalized_config.github.runner_owner
runner_registration_level = local.normalized_config.github.runner_registration_level
user_agent = local.normalized_config.github.user_agent
}

lambda = merge(v.lambda, {
Expand All @@ -35,6 +37,7 @@ locals {
artifact = local.normalized_config.orchestration_provider.webhook.lambda.artifact
})
})
scale_set = v.orchestration_provider.scale_set
}

ssm = merge(v.ssm, {
Expand Down
1 change: 1 addition & 0 deletions modules/multi-runner/config.experimental.resolved.tf
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ locals {
tags = merge(local.normalized_config.orchestration_provider.webhook.queue.tags, v.orchestration_provider.webhook.queue.tags)
})
})
scale_set = v.orchestration_provider.scale_set
}

ssm = merge(v.ssm, {
Expand Down
6 changes: 5 additions & 1 deletion modules/multi-runner/config.experimental.translation.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ locals {
url = var.ghes_url
ssl_verify = var.ghes_ssl_verify
}
user_agent = var.user_agent
runner_owner = null
runner_registration_level = "organization"
user_agent = var.user_agent
}

stable_to_v2_lambda = {
Expand Down Expand Up @@ -141,6 +143,7 @@ locals {
encryption = var.queue_encryption
}
}
scale_set = null
}

stable_to_v2_ssm = {
Expand Down Expand Up @@ -427,6 +430,7 @@ locals {
}
}
}
scale_set = null
}

ssm = {
Expand Down
5 changes: 3 additions & 2 deletions modules/multi-runner/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ locals {
"ghr:environment" = var.prefix
})

primary_app_id = coalesce(local.effective_config.github.app.id_ssm, module.ssm.parameters.github_app_id)
primary_app_key_base64 = coalesce(local.effective_config.github.app.key_base64_ssm, module.ssm.parameters.github_app_key_base64)
primary_app_id = coalesce(local.effective_config.github.app.id_ssm, module.ssm.parameters.github_app_id)
primary_app_key_base64 = coalesce(local.effective_config.github.app.key_base64_ssm, module.ssm.parameters.github_app_key_base64)
primary_app_installation_id = local.effective_config.github.app.installation_id_ssm != null ? local.effective_config.github.app.installation_id_ssm : module.ssm.parameters.github_app_installation_id

github_app_parameters = {
id = local.primary_app_id
Expand Down
Loading
Loading