Skip to content

feat: migrate launch template to use SSM for AMI lookup #4517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ Join our discord community via [this invite link](https://discord.gg/bxgXW8jJGh)
| <a name="input_ami_housekeeper_lambda_schedule_expression"></a> [ami\_housekeeper\_lambda\_schedule\_expression](#input\_ami\_housekeeper\_lambda\_schedule\_expression) | Scheduler expression for action runner binary syncer. | `string` | `"rate(1 day)"` | no |
| <a name="input_ami_housekeeper_lambda_timeout"></a> [ami\_housekeeper\_lambda\_timeout](#input\_ami\_housekeeper\_lambda\_timeout) | Time out of the lambda in seconds. | `number` | `300` | no |
| <a name="input_ami_housekeeper_lambda_zip"></a> [ami\_housekeeper\_lambda\_zip](#input\_ami\_housekeeper\_lambda\_zip) | File location of the lambda zip file. | `string` | `null` | no |
| <a name="input_ami_id_ssm_parameter_name"></a> [ami\_id\_ssm\_parameter\_name](#input\_ami\_id\_ssm\_parameter\_name) | Externally managed SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami\_filter | `string` | `null` | no |
| <a name="input_ami_id_ssm_parameter_arn"></a> [ami\_id\_ssm\_parameter\_arn](#input\_ami\_id\_ssm\_parameter\_arn) | ARN of the SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami\_filter | `string` | `null` | no |
| <a name="input_ami_id_ssm_parameter_name"></a> [ami\_id\_ssm\_parameter\_name](#input\_ami\_id\_ssm\_parameter\_name) | (DEPRECATED) Variable is replaced by `ami_id_ssm_parameter_arn` Externally managed SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami\_filter | `string` | `null` | no |
| <a name="input_ami_kms_key_arn"></a> [ami\_kms\_key\_arn](#input\_ami\_kms\_key\_arn) | Optional CMK Key ARN to be used to launch an instance from a shared encrypted AMI | `string` | `null` | no |
| <a name="input_ami_owners"></a> [ami\_owners](#input\_ami\_owners) | The list of owners used to select the AMI of action runner instances. | `list(string)` | <pre>[<br/> "amazon"<br/>]</pre> | no |
| <a name="input_associate_public_ipv4_address"></a> [associate\_public\_ipv4\_address](#input\_associate\_public\_ipv4\_address) | Associate public IPv4 with the runner. Only tested with IPv4 | `bool` | `false` | no |
Expand Down Expand Up @@ -141,7 +142,7 @@ Join our discord community via [this invite link](https://discord.gg/bxgXW8jJGh)
| <a name="input_eventbridge"></a> [eventbridge](#input\_eventbridge) | Enable the use of EventBridge by the module. By enabling this feature events will be put on the EventBridge by the webhook instead of directly dispatching to queues for scaling.<br/><br/> `enable`: Enable the EventBridge feature.<br/> `accept_events`: List can be used to only allow specific events to be putted on the EventBridge. By default all events, empty list will be be interpreted as all events. | <pre>object({<br/> enable = optional(bool, true)<br/> accept_events = optional(list(string), null)<br/> })</pre> | `{}` | no |
| <a name="input_ghes_ssl_verify"></a> [ghes\_ssl\_verify](#input\_ghes\_ssl\_verify) | GitHub Enterprise SSL verification. Set to 'false' when custom certificate (chains) is used for GitHub Enterprise Server (insecure). | `bool` | `true` | no |
| <a name="input_ghes_url"></a> [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB. However if you are using Github Enterprise Cloud with data-residency (ghe.com), set the endpoint here. Example - https://companyname.ghe.com | `string` | `null` | no |
| <a name="input_github_app"></a> [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. <br/> You can optionally create the SSM parameters yourself and provide the ARN and name here, through the `*_ssm` attributes.<br/> If you chose to provide the configuration values directly here, <br/> please ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`).<br/> Note: the provided SSM parameters arn and name have a precedence over the actual value (i.e `key_base64_ssm` has a precedence over `key_base64` etc). | <pre>object({<br/> key_base64 = optional(string)<br/> key_base64_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> id = optional(string)<br/> id_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> webhook_secret = optional(string)<br/> webhook_secret_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> })</pre> | n/a | yes |
| <a name="input_github_app"></a> [github\_app](#input\_github\_app) | GitHub app parameters, see your github app.<br/> You can optionally create the SSM parameters yourself and provide the ARN and name here, through the `*_ssm` attributes.<br/> If you chose to provide the configuration values directly here,<br/> please ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`).<br/> Note: the provided SSM parameters arn and name have a precedence over the actual value (i.e `key_base64_ssm` has a precedence over `key_base64` etc). | <pre>object({<br/> key_base64 = optional(string)<br/> key_base64_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> id = optional(string)<br/> id_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> webhook_secret = optional(string)<br/> webhook_secret_ssm = optional(object({<br/> arn = string<br/> name = string<br/> }))<br/> })</pre> | n/a | yes |
| <a name="input_idle_config"></a> [idle\_config](#input\_idle\_config) | List of time periods, defined as a cron expression, to keep a minimum amount of runners active instead of scaling down to 0. By defining this list you can ensure that in time periods that match the cron expression within 5 seconds a runner is kept idle. | <pre>list(object({<br/> cron = string<br/> timeZone = string<br/> idleCount = number<br/> evictionStrategy = optional(string, "oldest_first")<br/> }))</pre> | `[]` | no |
| <a name="input_instance_allocation_strategy"></a> [instance\_allocation\_strategy](#input\_instance\_allocation\_strategy) | The allocation strategy for spot instances. AWS recommends using `price-capacity-optimized` however the AWS default is `lowest-price`. | `string` | `"lowest-price"` | no |
| <a name="input_instance_max_spot_price"></a> [instance\_max\_spot\_price](#input\_instance\_max\_spot\_price) | Max price price for spot instances per hour. This variable will be passed to the create fleet as max spot price for the fleet. | `string` | `null` | no |
Expand Down
60 changes: 30 additions & 30 deletions examples/default/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/default/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ terraform output -raw webhook_secret

| Name | Version |
|------|---------|
| <a name="provider_random"></a> [random](#provider\_random) | 3.6.3 |
| <a name="provider_random"></a> [random](#provider\_random) | 3.7.1 |

## Modules

Expand Down
1 change: 1 addition & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ module "runners" {
runner_architecture = var.runner_architecture
ami_filter = var.ami_filter
ami_owners = var.ami_owners
ami_id_ssm_parameter_arn = var.ami_id_ssm_parameter_arn
ami_id_ssm_parameter_name = var.ami_id_ssm_parameter_name
ami_kms_key_arn = var.ami_kms_key_arn

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

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions modules/multi-runner/runners.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module "runners" {
runner_architecture = each.value.runner_config.runner_architecture
ami_filter = each.value.runner_config.ami_filter
ami_owners = each.value.runner_config.ami_owners
ami_id_ssm_parameter_arn = each.value.runner_config.ami_id_ssm_parameter_arn
ami_id_ssm_parameter_name = each.value.runner_config.ami_id_ssm_parameter_name
ami_kms_key_arn = each.value.runner_config.ami_kms_key_arn

Expand Down
5 changes: 3 additions & 2 deletions modules/multi-runner/variables.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
variable "github_app" {
description = <<EOF
GitHub app parameters, see your github app.
GitHub app parameters, see your github app.
You can optionally create the SSM parameters yourself and provide the ARN and name here, through the `*_ssm` attributes.
If you chose to provide the configuration values directly here,
If you chose to provide the configuration values directly here,
please ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`).
Note: the provided SSM parameters arn and name have a precedence over the actual value (i.e `key_base64_ssm` has a precedence over `key_base64` etc).
EOF
Expand Down Expand Up @@ -67,6 +67,7 @@ variable "multi_runner_config" {
})
ami_filter = optional(map(list(string)), { state = ["available"] })
ami_owners = optional(list(string), ["amazon"])
ami_id_ssm_parameter_arn = optional(string, null)
ami_id_ssm_parameter_name = optional(string, null)
ami_kms_key_arn = optional(string, "")
create_service_linked_role_spot = optional(bool, false)
Expand Down
2 changes: 2 additions & 0 deletions modules/runners/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ yarn run dist
| [aws_ssm_parameter.disable_default_labels](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.jit_config_enabled](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.runner_agent_mode](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.runner_ami_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.runner_config_run_as](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.runner_enable_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_ssm_parameter.token_path](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
Expand All @@ -133,6 +134,7 @@ yarn run dist
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_ami_filter"></a> [ami\_filter](#input\_ami\_filter) | Map of lists used to create the AMI filter for the action runner AMI. | `map(list(string))` | <pre>{<br/> "state": [<br/> "available"<br/> ]<br/>}</pre> | no |
| <a name="input_ami_id_ssm_parameter_arn"></a> [ami\_id\_ssm\_parameter\_arn](#input\_ami\_id\_ssm\_parameter\_arn) | ARN of the SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami\_filter | `string` | `null` | no |
| <a name="input_ami_id_ssm_parameter_name"></a> [ami\_id\_ssm\_parameter\_name](#input\_ami\_id\_ssm\_parameter\_name) | Externally managed SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami\_filter | `string` | `null` | no |
| <a name="input_ami_kms_key_arn"></a> [ami\_kms\_key\_arn](#input\_ami\_kms\_key\_arn) | Optional CMK Key ARN to be used to launch an instance from a shared encrypted AMI | `string` | `null` | no |
| <a name="input_ami_owners"></a> [ami\_owners](#input\_ami\_owners) | The list of owners used to select the AMI of action runner instances. | `list(string)` | <pre>[<br/> "amazon"<br/>]</pre> | no |
Expand Down
28 changes: 25 additions & 3 deletions modules/runners/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ locals {
"linux" = "${path.module}/templates/start-runner.sh"
}

ami_kms_key_arn = var.ami_kms_key_arn != null ? var.ami_kms_key_arn : ""
ami_filter = merge(local.default_ami[var.runner_os], var.ami_filter)
ami_kms_key_arn = var.ami_kms_key_arn != null ? var.ami_kms_key_arn : ""
ami_filter = merge(local.default_ami[var.runner_os], var.ami_filter)
ami_id_ssm_module_managed = var.ami_id_ssm_parameter_arn == null

enable_job_queued_check = var.enable_job_queued_check == null ? !var.enable_ephemeral_runners : var.enable_job_queued_check

Expand Down Expand Up @@ -84,6 +85,27 @@ data "aws_ami" "runner" {
owners = var.ami_owners
}

resource "aws_ssm_parameter" "runner_ami_id" {
count = local.ami_id_ssm_module_managed ? 1 : 0
name = "${var.ssm_paths.root}/${var.ssm_paths.config}/ami_id"
type = "String"
data_type = "aws:ec2:image"
value = data.aws_ami.runner.id

tags = merge(
local.tags,
{
"ghr:ami_name" = data.aws_ami.runner.name
},
{
"ghr:ami_creation_date" = data.aws_ami.runner.creation_date
},
{
"ghr:ami_deprecation_time" = data.aws_ami.runner.deprecation_time
}
)
}

resource "aws_launch_template" "runner" {
name = "${var.prefix}-action-runner"

Expand Down Expand Up @@ -140,7 +162,7 @@ resource "aws_launch_template" "runner" {
}

instance_initiated_shutdown_behavior = "terminate"
image_id = data.aws_ami.runner.id
image_id = "resolve:ssm:${local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : var.ami_id_ssm_parameter_arn}"
key_name = var.key_name
ebs_optimized = var.ebs_optimized

Expand Down
9 changes: 9 additions & 0 deletions modules/runners/policies/lambda-scale-up.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
"${ssm_config_path}/*"
]
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParameters"
],
"Resource": [
"${ssm_ami_id_parameter_arn}"
]
},
{
"Effect": "Allow",
"Action": [
Expand Down
1 change: 1 addition & 0 deletions modules/runners/scale-up.tf
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ resource "aws_iam_role_policy" "scale_up" {
ssm_config_path = "arn:${var.aws_partition}:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter${var.ssm_paths.root}/${var.ssm_paths.config}"
kms_key_arn = local.kms_key_arn
ami_kms_key_arn = local.ami_kms_key_arn
ssm_ami_id_parameter_arn = local.ami_id_ssm_module_managed ? aws_ssm_parameter.runner_ami_id[0].arn : var.ami_id_ssm_parameter_arn
})
}

Expand Down
6 changes: 6 additions & 0 deletions modules/runners/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ variable "ami_owners" {
default = ["amazon"]
}

variable "ami_id_ssm_parameter_arn" {
description = "ARN of the SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami_filter"
type = string
default = null
}

variable "ami_id_ssm_parameter_name" {
description = "Externally managed SSM parameter (of data type aws:ec2:image) that contains the AMI ID to launch runner instances from. Overrides ami_filter"
type = string
Expand Down
Loading