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
26 changes: 26 additions & 0 deletions docs/adr/002-runner-orchestration-provider-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,19 @@ entries are present, `experimental_features = ["multi-runner-v2"]` is required;
the v2 path combines native v2 entries with translated v1 entries from the
same map.

The experimental AWS EC2 provider intentionally remains separate from the
stable `modules/runners` implementation. It owns its own Terraform resource
definitions and copies of the runner bootstrap templates so the v2 provider
boundary can evolve without changing the stable module's public contract,
resource graph, state addresses, or deployed behavior. The template bodies are
equivalent at introduction; eight of the ten files are byte-identical, while
`cloudwatch_config.json` and `user-data.ps1` differ only by a final newline.

This isolation temporarily duplicates Terraform and template code. Equivalent
fixes must be evaluated for both implementations, and review should explicitly
check for unintended drift. Consolidation is deferred until stable v1 is
deprecated and a migration can be designed and verified independently.

The canonical v2 output groups orchestration resources under
`orchestration_provider.webhook` and compute resources under the selected
namespace and provider, currently `provider.aws.ec2`. Compatibility aliases
Expand Down Expand Up @@ -333,6 +346,9 @@ It does not permit both controllers to own the same runner configuration.
- Global webhook defaults and per-runner webhook selection have similarly named
blocks with different purposes.
- Adapter objects and capability contracts require maintenance.
- Keeping the stable and experimental EC2 implementations isolated temporarily
duplicates Terraform and bootstrap templates, increasing review and
maintenance effort until v1 is deprecated.
- A stateful provider will still require separate runtime, deployment,
observability, and failure-recovery design.

Expand Down Expand Up @@ -362,6 +378,16 @@ support components it does not use.

**Decision**: Keep those leaves under the webhook provider root.

### Reuse or refactor the stable runners module now

Reusing `modules/runners` or extracting shared implementation code now would
reduce duplication, but it would couple the experimental provider refactor to
the stable module's compatibility and Terraform-state contract.

**Decision**: Keep the v2 EC2 provider isolated and accept temporary
duplication. Consolidate only after stable v1 deprecation and a separately
reviewed migration.

### Add the scale-set schema and ECS service now

Publishing placeholders would lock in names and types before the API client,
Expand Down
80 changes: 80 additions & 0 deletions modules/compute-providers/aws/ec2/README.md

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions modules/compute-providers/aws/ec2/control-plane.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# EC2-specific IAM and environment fragments consumed by the common control
# plane in runner-config.
data "aws_iam_policy_document" "ami_id_ssm_parameter_read" {
count = local.ami_id_ssm_external ? 1 : 0

statement {
effect = "Allow"
actions = ["ssm:GetParameter"]
resources = [local.ami_id_ssm_parameter_arn]
}
}

resource "aws_iam_policy" "ami_id_ssm_parameter_read" {
count = local.ami_id_ssm_external ? 1 : 0
name = "${var.prefix}-ami-id-ssm-parameter-read"
path = local.role_path
description = "Allows for reading ${var.prefix} GitHub runner AMI ID from an SSM parameter"
tags = local.provider_tags
policy = data.aws_iam_policy_document.ami_id_ssm_parameter_read[0].json
}

data "aws_iam_policy_document" "scale_up" {
statement {
effect = "Allow"
actions = [
"ec2:DescribeInstances",
"ec2:DescribeLaunchTemplateVersions",
"ec2:DescribeTags",
"ec2:RunInstances",
"ec2:CreateFleet",
"ec2:CreateTags",
]
resources = ["*"]
}

statement {
effect = "Allow"
actions = ["ec2:TerminateInstances"]
resources = ["*"]

condition {
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:Application"
values = ["github-action-runner"]
}
Comment thread
edersonbrilhante marked this conversation as resolved.
}

statement {
effect = "Allow"
actions = ["ec2:TerminateInstances"]
resources = ["*"]

condition {
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:environment"
values = [var.prefix]
}
}

statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = [var.runner.iam.role.arn]
}

statement {
effect = "Allow"
actions = ["ssm:GetParameter", "ssm:GetParameters"]
resources = [local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn]
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
actions = ["kms:DescribeKey", "kms:ReEncrypt*", "kms:Decrypt"]
resources = [statement.value]
}
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
actions = ["kms:CreateGrant"]
resources = [statement.value]

condition {
test = "Bool"
variable = "aws:ViaAWSService"
values = ["true"]
}
}
}
}

data "aws_iam_policy_document" "scale_down" {
statement {
effect = "Allow"
actions = ["ec2:DescribeInstances", "ec2:DescribeTags"]
resources = ["*"]
}

statement {
effect = "Allow"
actions = ["ec2:TerminateInstances", "ec2:CreateTags", "ec2:DeleteTags"]
resources = ["*"]

condition {
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:Application"
values = ["github-action-runner"]
}
}

statement {
effect = "Allow"
actions = ["ec2:TerminateInstances", "ec2:CreateTags", "ec2:DeleteTags"]
resources = ["*"]

condition {
test = "StringEquals"
variable = "ec2:ResourceTag/ghr:environment"
values = [var.prefix]
}
}
}

data "aws_iam_policy_document" "pool" {
statement {
effect = "Allow"
actions = [
"ec2:DescribeInstances",
"ec2:DescribeTags",
"ec2:RunInstances",
"ec2:CreateFleet",
"ec2:CreateTags",
]
resources = ["*"]
}

statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = [var.runner.iam.role.arn]
}

statement {
effect = "Allow"
actions = ["ssm:GetParameters"]
resources = [local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : local.ami_id_ssm_parameter_arn]
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
actions = ["kms:DescribeKey", "kms:ReEncrypt*", "kms:Decrypt"]
resources = [statement.value]
}
}

dynamic "statement" {
for_each = local.ami_kms_key_enabled ? [local.ami_kms_key_arn] : []

content {
effect = "Allow"
actions = ["kms:CreateGrant"]
resources = [statement.value]

condition {
test = "Bool"
variable = "aws:ViaAWSService"
values = ["true"]
}
}
}
}

data "aws_iam_policy_document" "service_linked_role" {
count = var.config.create_service_linked_role_spot ? 1 : 0

statement {
effect = "Allow"
actions = ["iam:CreateServiceLinkedRole"]
resources = ["arn:${var.aws_partition}:iam::*:role/aws-service-role/*"]
}
}

locals {
scale_up_environment_variables = {
AMI_ID_SSM_PARAMETER_NAME = local.ami_id_ssm_parameter_name
INSTANCE_ALLOCATION_STRATEGY = var.config.instance_allocation_strategy
INSTANCE_MAX_SPOT_PRICE = var.config.instance_max_spot_price
INSTANCE_TARGET_CAPACITY_TYPE = var.config.instance_target_capacity_type
INSTANCE_TYPE_PRIORITIES = var.config.instance_type_priorities != null ? jsonencode(var.config.instance_type_priorities) : ""
INSTANCE_TYPES = join(",", var.config.instance_types)
LAUNCH_TEMPLATE_NAME = aws_launch_template.runner.name
SUBNET_IDS = join(",", var.config.subnet_ids)
ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.config.on_demand_failover_for_errors)
SCALE_ERRORS = jsonencode(var.config.scale_errors)
USE_DEDICATED_HOST = var.config.use_dedicated_host
}

scale_down_environment_variables = {}

pool_environment_variables = local.scale_up_environment_variables

scale_up_iam_policy_json = data.aws_iam_policy_document.scale_up.json
scale_down_iam_policy_json = data.aws_iam_policy_document.scale_down.json
pool_iam_policy_json = data.aws_iam_policy_document.pool.json
service_linked_role_policy_json = var.config.create_service_linked_role_spot ? data.aws_iam_policy_document.service_linked_role[0].json : null
}
9 changes: 9 additions & 0 deletions modules/compute-providers/aws/ec2/instance-profile.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The common runner configuration owns the role; EC2 owns the profile consumed by its
# launch template.
resource "aws_iam_instance_profile" "runner" {
count = var.config.instance_profile == null ? 1 : 0
name = "${var.prefix}-runner-profile"
role = var.runner.iam.role.name
path = local.instance_profile_path
tags = local.provider_tags
}
75 changes: 75 additions & 0 deletions modules/compute-providers/aws/ec2/logging.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# EC2 runner log collection and CloudWatch resources.
locals {
runner_log_files = (
var.config.log_files != null
? var.config.log_files
: [
{
"prefix_log_group" : true,
"file_path" : "/var/log/messages",
"log_group_name" : "messages",
"log_stream_name" : "{instance_id}",
"log_class" : "STANDARD"
},
{
"log_group_name" : "user_data",
"prefix_log_group" : true,
"file_path" : var.runner.os == "windows" ? "C:/UserData.log" : "/var/log/user-data.log",
"log_stream_name" : "{instance_id}",
"log_class" : "STANDARD"
},
{
"log_group_name" : "runner",
"prefix_log_group" : true,
"file_path" : var.runner.os == "windows" ? "C:/actions-runner/_diag/Runner_*.log" : "/opt/actions-runner/_diag/Runner_**.log",
"log_stream_name" : "{instance_id}",
"log_class" : "STANDARD"
},
{
"log_group_name" : "runner-startup",
"prefix_log_group" : true,
"file_path" : var.runner.os == "windows" ? "C:/runner-startup.log" : "/var/log/runner-startup.log",
"log_stream_name" : "{instance_id}",
"log_class" : "STANDARD"
}
]
)
# CloudWatch agent collect_list schema expects log_group_class, not log_class
logfiles = var.config.cloudwatch_agent.enabled ? [for l in local.runner_log_files : {
"log_group_name" : l.prefix_log_group ? "/github-self-hosted-runners/${var.prefix}/${l.log_group_name}" : "/${l.log_group_name}"
"log_stream_name" : l.log_stream_name
"file_path" : l.file_path
"log_group_class" : l.log_class
}] : []

loggroups_names = distinct([for l in local.logfiles : l.log_group_name])
# Create a list of unique log classes corresponding to each log group name
# This maintains the same order as loggroups_names for use with count
loggroups_classes = [
for name in local.loggroups_names : [
for l in local.logfiles : l.log_group_class
if l.log_group_name == name
][0]
]

}


resource "aws_ssm_parameter" "cloudwatch_agent_config_runner" {
count = var.config.cloudwatch_agent.enabled ? 1 : 0
name = "${var.ssm.paths.root}/${var.ssm.paths.config}/cloudwatch_agent_config_runner"
type = "String"
value = var.config.cloudwatch_agent.config != null ? var.config.cloudwatch_agent.config : templatefile("${path.module}/templates/cloudwatch_config.json", {
logfiles = jsonencode(local.logfiles)
})
tags = local.ssm_parameter_tags
}

resource "aws_cloudwatch_log_group" "gh_runners" {
count = length(local.loggroups_names)
name = local.loggroups_names[count.index]
retention_in_days = var.observability.logs.retention_in_days
kms_key_id = var.observability.logs.kms_key_id
log_group_class = local.loggroups_classes[count.index]
tags = local.log_group_tags
}
23 changes: 23 additions & 0 deletions modules/compute-providers/aws/ec2/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
output "environment_variables" {
description = "Provider-specific Lambda environment variable fragments consumed by runner-config."
value = local.provider_environment_variables
}

output "policies" {
description = "Provider-specific IAM policy fragments consumed by runner-config."
value = local.provider_policies
}

output "resources" {
description = "Provider-specific EC2 resources exposed by runner-config."
value = local.provider_resources
}

output "provider" {
description = "Nested EC2 compute-provider contract consumed by runner-config."
value = {
environment_variables = local.provider_environment_variables
policies = local.provider_policies
resources = local.provider_resources
}
}
Loading