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
12 changes: 6 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ Furthermore, to accommodate larger JIT configurations or other stored values, th
- **Cost Implications**: While the 'Standard' tier is generally free for a certain number of parameters and operations, the 'Advanced' tier incurs costs. These costs are typically pro-rated per hour for each parameter stored using the Advanced tier. For detailed and up-to-date pricing, please refer to the [AWS Systems Manager Pricing page](https://aws.amazon.com/systems-manager/pricing/#Parameter_Store).
- **Housekeeping Recommendation**: The last sentence of the "AWS SSM Parameters" section already mentions that "data not deleted after a day will be deleted by a SSM housekeeper lambda." It is crucial to ensure this or a similar housekeeping mechanism is active and correctly configured, especially considering the potential costs associated with 'Advanced' tier parameters. This utility should identify and delete any orphaned parameters to help manage costs and maintain a clean SSM environment.

| Path | Description |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ssm_paths.root/var.prefix?/app/` | App secrets used by Lambda's |
| `ssm_paths.root/var.prefix?/runners/config/<name>` | Configuration parameters used by runner start script |
| `ssm_paths.root/var.prefix?/runners/tokens/<ec2-instance-id>` | Either JIT configuration (ephemeral runners) or registration tokens (non ephemeral runners) generated by the control plane (scale-up lambda), and consumed by the start script on the runner to activate / register the runner. |
| `ssm_paths.root/var.prefix?/webhook/runner-matcher-config` | Runner matcher config used by webhook to decide the target for the webhook event. |
| Path | Description |
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ssm_paths.root/var.prefix?/app/` | App secrets used by Lambda's |
| `ssm_paths.root/var.prefix?/runners/config/<name>` | Configuration parameters used by runner start script |
| `ssm_paths.root/var.prefix?/runners/tokens/<ec2-instance-id>` | Either JIT configuration (ephemeral runners) or registration tokens (non ephemeral runners) generated by the control plane (scale-up lambda), and consumed by the start script on the runner to activate / register the runner. |
| `ssm_paths.root/var.prefix?/webhook/runner-matcher-config-<hash>-<index>` | Runner matcher config used by webhook to decide the target for the webhook event. Split into one or more parameters. The names change with the content, so a changed config is written to new parameters before the old ones are deleted. |

Available configuration parameters:

Expand Down
218 changes: 218 additions & 0 deletions modules/multi-runner/tests/webhook-matcher-config.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
mock_provider "aws" {
mock_data "aws_caller_identity" {
defaults = {
account_id = "123456789012"
}
}

mock_data "aws_iam_policy_document" {
defaults = {
json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}"
}
}

mock_resource "aws_iam_role" {
defaults = {
arn = "arn:aws:iam::123456789012:role/test-role"
}
}

mock_resource "aws_cloudwatch_event_bus" {
defaults = {
arn = "arn:aws:events:eu-west-1:123456789012:event-bus/test"
}
}

mock_resource "aws_cloudwatch_event_rule" {
defaults = {
arn = "arn:aws:events:eu-west-1:123456789012:rule/test"
}
}

mock_resource "aws_lambda_function" {
defaults = {
arn = "arn:aws:lambda:eu-west-1:123456789012:function:test"
}
}

mock_resource "aws_sqs_queue" {
defaults = {
arn = "arn:aws:sqs:eu-west-1:123456789012:test"
}
}

mock_resource "aws_s3_bucket" {
defaults = {
arn = "arn:aws:s3:::test-runner-binaries"
id = "test-runner-binaries"
}
}

mock_resource "aws_apigatewayv2_api" {
defaults = {
execution_arn = "arn:aws:execute-api:eu-west-1:123456789012:test"
}
}
}

mock_provider "random" {}
mock_provider "null" {}

variables {
aws_region = "eu-west-1"
vpc_id = "vpc-test"
subnet_ids = ["subnet-test"]

multi_runner_config = {}

github_app = {
key_base64_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/key"
name = "/tests/github-app/key"
}
id_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/id"
name = "/tests/github-app/id"
}
webhook_secret_ssm = {
arn = "arn:aws:ssm:eu-west-1:123456789012:parameter/tests/github-app/webhook-secret"
name = "/tests/github-app/webhook-secret"
}
}

lambda_s3_bucket = "test-lambda-artifacts"
runners_lambda_zip = "README.md"
runners_lambda_s3_key = "runners.zip"
webhook_lambda_s3_key = "webhook.zip"
syncer_lambda_s3_key = "runner-binaries-syncer.zip"
}

run "matcher_config_chunk_names_include_config_hash" {
command = apply

variables {
multi_runner_config = {
stable = {
runner_config = {
runner_os = "linux"
runner_architecture = "x64"
instance_types = ["m5.large"]
runners_maximum_count = 1
}
matcherConfig = {
labelMatchers = [["first-label"]]
}
}
}
}

assert {
condition = alltrue([
for path in split(":", output.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"]) :
can(regex("/runner-matcher-config-[0-9a-f]{12}-[0-9]+$", path))
])
error_message = "Matcher config chunk names must contain the config hash and the chunk index."
}
}

run "unchanged_matcher_config_keeps_chunk_names" {
command = apply

variables {
previous_matcher_config_path = run.matcher_config_chunk_names_include_config_hash.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"]

multi_runner_config = {
stable = {
runner_config = {
runner_os = "linux"
runner_architecture = "x64"
instance_types = ["m5.large"]
runners_maximum_count = 1
}
matcherConfig = {
labelMatchers = [["first-label"]]
}
}
}
}

assert {
condition = output.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"] == var.previous_matcher_config_path
error_message = "An unchanged matcher config must keep the same chunk names."
}
}

run "changed_matcher_config_uses_new_chunk_names" {
command = apply

variables {
previous_matcher_config_path = run.unchanged_matcher_config_keeps_chunk_names.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"]

multi_runner_config = {
stable = {
runner_config = {
runner_os = "linux"
runner_architecture = "x64"
instance_types = ["m5.large"]
runners_maximum_count = 1
}
matcherConfig = {
labelMatchers = [["second-label"]]
}
}
}
}

assert {
condition = output.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"] != var.previous_matcher_config_path
error_message = "A changed matcher config must use new chunk names."
}
}

run "restore_original_matcher_config" {
command = apply

variables {
multi_runner_config = {
stable = {
runner_config = {
runner_os = "linux"
runner_architecture = "x64"
instance_types = ["m5.large"]
runners_maximum_count = 1
}
matcherConfig = {
labelMatchers = [["first-label"]]
}
}
}
}
}

run "changed_parameter_tier_uses_new_chunk_names" {
command = apply

variables {
previous_matcher_config_path = run.restore_original_matcher_config.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"]

matcher_config_parameter_store_tier = "Advanced"
multi_runner_config = {
stable = {
runner_config = {
runner_os = "linux"
runner_architecture = "x64"
instance_types = ["m5.large"]
runners_maximum_count = 1
}
matcherConfig = {
labelMatchers = [["first-label"]]
}
}
}
}

assert {
condition = output.webhook.lambda.environment[0].variables["PARAMETER_RUNNER_MATCHER_CONFIG_PATH"] != var.previous_matcher_config_path
error_message = "A changed parameter tier must use new chunk names."
}
}
2 changes: 2 additions & 0 deletions modules/webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ yarn run dist
| [aws_apigatewayv2_route.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_route) | resource |
| [aws_apigatewayv2_stage.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stage) | resource |
| [aws_ssm_parameter.runner_matcher_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |

## Inputs

Expand Down
2 changes: 1 addition & 1 deletion modules/webhook/direct/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ No modules.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_config"></a> [config](#input\_config) | Configuration object for all variables. | <pre>object({<br/> prefix = string<br/> archive = optional(object({<br/> enable = optional(bool, true)<br/> retention_days = optional(number, 7)<br/> }), {})<br/> tags = optional(map(string), {})<br/><br/> lambda_subnet_ids = optional(list(string), [])<br/> lambda_security_group_ids = optional(list(string), [])<br/> sqs_job_queues_arns = list(string)<br/> lambda_zip = optional(string, null)<br/> lambda_memory_size = optional(number, 256)<br/> lambda_timeout = optional(number, 10)<br/> role_permissions_boundary = optional(string, null)<br/> role_path = optional(string, null)<br/> logging_retention_in_days = optional(number, 180)<br/> logging_kms_key_id = optional(string, null)<br/> log_class = optional(string, "STANDARD")<br/> lambda_s3_bucket = optional(string, null)<br/> lambda_s3_key = optional(string, null)<br/> lambda_s3_object_version = optional(string, null)<br/> lambda_apigateway_access_log_settings = optional(object({<br/> destination_arn = string<br/> format = string<br/> }), null)<br/> repository_white_list = optional(list(string), [])<br/> queue_selection_strategy = optional(string, "first")<br/> storage_provider = object({<br/> aws = object({<br/> ssm = object({<br/> kms_key_id = optional(string, null)<br/> })<br/> })<br/> })<br/> log_level = optional(string, "info")<br/> lambda_runtime = optional(string, "nodejs24.x")<br/> aws_partition = optional(string, "aws")<br/> lambda_architecture = optional(string, "arm64")<br/> github_app_parameters = object({<br/> webhook_secret = map(string)<br/> })<br/> tracing_config = optional(object({<br/> mode = optional(string, null)<br/> capture_http_requests = optional(bool, false)<br/> capture_error = optional(bool, false)<br/> }), {})<br/> lambda_tags = optional(map(string), {})<br/> api_gw_source_arn = string<br/> ssm_parameter_runner_matcher_config = list(object({<br/> name = string<br/> arn = string<br/> version = string<br/> }))<br/> })</pre> | n/a | yes |
| <a name="input_config"></a> [config](#input\_config) | Configuration object for all variables. | <pre>object({<br/> prefix = string<br/> archive = optional(object({<br/> enable = optional(bool, true)<br/> retention_days = optional(number, 7)<br/> }), {})<br/> tags = optional(map(string), {})<br/><br/> lambda_subnet_ids = optional(list(string), [])<br/> lambda_security_group_ids = optional(list(string), [])<br/> sqs_job_queues_arns = list(string)<br/> lambda_zip = optional(string, null)<br/> lambda_memory_size = optional(number, 256)<br/> lambda_timeout = optional(number, 10)<br/> role_permissions_boundary = optional(string, null)<br/> role_path = optional(string, null)<br/> logging_retention_in_days = optional(number, 180)<br/> logging_kms_key_id = optional(string, null)<br/> log_class = optional(string, "STANDARD")<br/> lambda_s3_bucket = optional(string, null)<br/> lambda_s3_key = optional(string, null)<br/> lambda_s3_object_version = optional(string, null)<br/> lambda_apigateway_access_log_settings = optional(object({<br/> destination_arn = string<br/> format = string<br/> }), null)<br/> repository_white_list = optional(list(string), [])<br/> queue_selection_strategy = optional(string, "first")<br/> storage_provider = object({<br/> aws = object({<br/> ssm = object({<br/> kms_key_id = optional(string, null)<br/> })<br/> })<br/> })<br/> log_level = optional(string, "info")<br/> lambda_runtime = optional(string, "nodejs24.x")<br/> aws_partition = optional(string, "aws")<br/> lambda_architecture = optional(string, "arm64")<br/> github_app_parameters = object({<br/> webhook_secret = map(string)<br/> })<br/> tracing_config = optional(object({<br/> mode = optional(string, null)<br/> capture_http_requests = optional(bool, false)<br/> capture_error = optional(bool, false)<br/> }), {})<br/> lambda_tags = optional(map(string), {})<br/> api_gw_source_arn = string<br/> ssm_parameter_runner_matcher_config = list(object({<br/> name = string<br/> arn = string<br/> version = string<br/> }))<br/> ssm_parameter_runner_matcher_config_arn_pattern = string<br/> })</pre> | n/a | yes |

## Outputs

Expand Down
1 change: 1 addition & 0 deletions modules/webhook/direct/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ variable "config" {
arn = string
version = string
}))
ssm_parameter_runner_matcher_config_arn_pattern = string
})
}
4 changes: 2 additions & 2 deletions modules/webhook/direct/webhook.tf
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ resource "aws_lambda_function" "webhook" {
memory_size = var.config.lambda_memory_size
timeout = var.config.lambda_timeout
architectures = [var.config.lambda_architecture]
depends_on = [aws_cloudwatch_log_group.webhook]
depends_on = [aws_cloudwatch_log_group.webhook, aws_iam_role_policy.webhook_ssm]

environment {
variables = merge({
Expand Down Expand Up @@ -141,7 +141,7 @@ resource "aws_iam_role_policy" "webhook_ssm" {
resource_arns = jsonencode(
concat(
[var.config.github_app_parameters.webhook_secret.arn],
[for p in var.config.ssm_parameter_runner_matcher_config : p.arn]
[var.config.ssm_parameter_runner_matcher_config_arn_pattern]
Comment thread
atsikham marked this conversation as resolved.
Resolved
)
)
})
Expand Down
Loading
Loading