diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl
index b4043fb5eb..61e1824697 100644
--- a/.terraform.lock.hcl
+++ b/.terraform.lock.hcl
@@ -3,7 +3,7 @@
provider "registry.terraform.io/hashicorp/aws" {
version = "5.31.0"
- constraints = "~> 5.27"
+ constraints = "~> 5.0, ~> 5.27"
hashes = [
"h1:ltxyuBWIy9cq0kIKDJH1jeWJy/y7XJLjS4QrsQK4plA=",
"zh:0cdb9c2083bf0902442384f7309367791e4640581652dda456f2d6d7abf0de8d",
@@ -24,6 +24,26 @@ provider "registry.terraform.io/hashicorp/aws" {
]
}
+provider "registry.terraform.io/hashicorp/null" {
+ version = "3.2.3"
+ constraints = "~> 3.2"
+ hashes = [
+ "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=",
+ "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2",
+ "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d",
+ "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3",
+ "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f",
+ "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301",
+ "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670",
+ "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed",
+ "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65",
+ "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd",
+ "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5",
+ ]
+}
+
provider "registry.terraform.io/hashicorp/random" {
version = "3.6.0"
constraints = "~> 3.0"
diff --git a/README.md b/README.md
index 2c2283f711..2d734da30d 100644
--- a/README.md
+++ b/README.md
@@ -156,7 +156,8 @@ Talk to the forestkeepers in the `runners-channel` on Slack.
| [enable\_ssm\_on\_runners](#input\_enable\_ssm\_on\_runners) | Enable to allow access to the runner instances for debugging purposes via SSM. Note that this adds additional permissions to the runner instances. | `bool` | `false` | no |
| [enable\_user\_data\_debug\_logging\_runner](#input\_enable\_user\_data\_debug\_logging\_runner) | Option to enable debug logging for user-data, this logs all secrets as well. | `bool` | `false` | no |
| [enable\_userdata](#input\_enable\_userdata) | Should the userdata script be enabled for the runner. Set this to false if you are using your own prebuilt AMI. | `bool` | `true` | no |
-| [enable\_workflow\_job\_events\_queue](#input\_enable\_workflow\_job\_events\_queue) | Enabling this experimental feature will create a secondory sqs queue to which a copy of the workflow\_job event will be delivered. | `bool` | `false` | no |
+| [enable\_workflow\_job\_events\_queue](#input\_enable\_workflow\_job\_events\_queue) | Enabling this experimental feature will create a secondary SQS queue to which a copy of the workflow\_job event will be delivered. | `bool` | `false` | no |
+| [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.
`enable`: Enable the EventBridge feature.
`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. |
object({| `{}` | no | | [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 | | [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB | `string` | `null` | no | | [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
enable = optional(bool, false)
accept_events = optional(list(string), null)
})
object({| n/a | yes | diff --git a/docs/assets/aws-architecture.dark.png b/docs/assets/aws-architecture.dark.png index ea6d3f38a6..ae5869760e 100644 Binary files a/docs/assets/aws-architecture.dark.png and b/docs/assets/aws-architecture.dark.png differ diff --git a/docs/assets/aws-architecture.light.png b/docs/assets/aws-architecture.light.png index 8fc065cf0b..79dbce4b91 100644 Binary files a/docs/assets/aws-architecture.light.png and b/docs/assets/aws-architecture.light.png differ diff --git a/docs/configuration.md b/docs/configuration.md index 4b5f33507e..6d74b1fe6a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,7 +6,7 @@ To be able to support a number of use-cases, the module has quite a lot of confi - Org vs Repo level. You can configure the module to connect the runners in GitHub on an org level and share the runners in your org, or set the runners on repo level and the module will install the runner to the repo. There can be multiple repos but runners are not shared between repos. - Multi-Runner module. This modules allows you to create multiple runner configurations with a single webhook and single GitHub App to simplify deployment of different types of runners. Check the detailed module [documentation](modules/public/multi-runner.md) for more information or checkout the [multi-runner example](examples/multi-runner.md). -- Workflow job event. You can configure the webhook in GitHub to send workflow job events to the webhook. Workflow job events were introduced by GitHub in September 2021 and are designed to support scalable runners. We advise using the workflow job event when possible. +- Webhook mode, the module can be deployed in `direct` mode or `EventBridge` (Experimental) mode. The `direct` mode is the default and will directly distribute to SQS for the scale-up lambda. The `EventBridge` mode will publish the events to a eventbus, the rule then directs the received events to a dispatch lambda. The dispatch lambda will send the event to the SQS queue. The `EventBridge` mode is useful when you want to have more control over the events and potentially filter them. The `EventBridge` mode is disabled by default. An example of what the `EventBridge` mode could be used for is building a data lake, build metrics, act on `workflow_job` job started events, etc. - Linux vs Windows. You can configure the OS types linux and win. Linux will be used by default. - Re-use vs Ephemeral. By default runners are re-used, until detected idle. Once idle they will be removed from the pool. To improve security we are introducing ephemeral runners. Those runners are only used for one job. Ephemeral runners only work in combination with the workflow job event. For ephemeral runners the lambda requests a JIT (just in time) configuration via the GitHub API to register the runner. [JIT configuration](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-just-in-time-runners) is limited to ephemeral runners (and currently not supported by GHES). For non-ephemeral runners, a registration token is always requested. In both cases the configuration is made available to the instance via the same SSM parameter. To disable JIT configuration for ephemeral runners set `enable_jit_config` to `false`. We also suggest using a pre-build AMI to improve the start time of jobs for ephemeral runners. - Job retry (**Beta**). By default the scale-up lambda will discard the message when it is handled. Meaning in the ephemeral use-case an instance is created. The created runner will ask GitHub for a job, no guarantee it will run the job for which it was scaling. Result could be that with small system hick-up the job is keeping waiting for a runner. Enable a pool (org runners) is one option to avoid this problem. Another option is to enable the job retry function. Which will retry the job after a delay for a configured number of times. @@ -259,8 +259,83 @@ Below an example of the the log messages created. } ``` +### EventBridge + +This module can be deployed in using the mode `EventBridge` (Experimental). The `EventBridge` mode will publish an event to a eventbus. Within the eventbus, there is a target rule set, sending events to the dispatch lambda. The `EventBridge` mode is disabled by default. + +Example to use the EventBridge: + +```hcl + +module "runners" { + source = "philips-labs/github-runners/aws" + + ... + eventbridge = { + enable = true + } + ... +} + +locals { + event_bus_name = module.runners.webhook.eventbridge.event_bus.name +} + +resource "aws_cloudwatch_event_rule" "example" { + name = "${local.prefix}-github-events-all" + description = "Caputure all GitHub events" + event_bus_name = local.event_bus_name + event_pattern = <
key_base64 = string
id = string
webhook_secret = string
})
object({| `{}` | no | | [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 | | [ghes\_url](#input\_ghes\_url) | GitHub Enterprise Server URL. Example: https://github.internal.co - DO NOT SET IF USING PUBLIC GITHUB | `string` | `null` | no | | [github\_app](#input\_github\_app) | GitHub app parameters, see your github app. Ensure the key is the base64-encoded `.pem` file (the output of `base64 app.private-key.pem`, not the content of `private-key.pem`). |
enable = optional(bool, false)
accept_events = optional(list(string), [])
})
object({| n/a | yes | diff --git a/modules/multi-runner/outputs.tf b/modules/multi-runner/outputs.tf index bc6624be27..0a7b99243f 100644 --- a/modules/multi-runner/outputs.tf +++ b/modules/multi-runner/outputs.tf @@ -38,6 +38,9 @@ output "webhook" { lambda_log_group = module.webhook.lambda_log_group lambda_role = module.webhook.role endpoint = "${module.webhook.gateway.api_endpoint}/${module.webhook.endpoint_relative_path}" + webhook = module.webhook.webhook + dispatcher = var.eventbridge.enable ? module.webhook.dispatcher : null + eventbridge = var.eventbridge.enable ? module.webhook.eventbridge : null } } diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index 553cc04594..a9d0a9f906 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -551,7 +551,7 @@ variable "pool_lambda_reserved_concurrent_executions" { } variable "enable_workflow_job_events_queue" { - description = "Enabling this experimental feature will create a secondory sqs queue to which a copy of the workflow_job event will be delivered." + description = "Enabling this experimental feature will create a secondary SQS queue to which a copy of the workflow_job event will be delivered." type = bool default = false } @@ -683,3 +683,13 @@ variable "metrics" { }) default = {} } + +variable "eventbridge" { + description = "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." + type = object({ + enable = optional(bool, false) + accept_events = optional(list(string), []) + }) + + default = {} +} diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index 573ebafbe0..9e70ca81a2 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -1,11 +1,12 @@ module "webhook" { - source = "../webhook" - prefix = var.prefix - tags = local.tags - kms_key_arn = var.kms_key_arn - + source = "../webhook" + prefix = var.prefix + tags = local.tags + kms_key_arn = var.kms_key_arn + eventbridge = var.eventbridge runner_matcher_config = local.runner_config matcher_config_parameter_store_tier = var.matcher_config_parameter_store_tier + ssm_paths = { root = local.ssm_root_path webhook = var.ssm_paths.webhook diff --git a/modules/webhook/README.md b/modules/webhook/README.md index 4419b109d0..4408bc56af 100644 --- a/modules/webhook/README.md +++ b/modules/webhook/README.md @@ -2,11 +2,11 @@ > This module is treated as internal module, breaking changes will not trigger a major release bump. -This module creates an API gateway endpoint and lambda function to handle GitHub App webhook events. +The module can be deployed in two modes. 'Direct' messages, are delivered directly to the runner queues. 'EventBridge' messages are delivered to an EventBridge bus and then dispatched to the runner queues. ## Lambda Function -The Lambda function is written in [TypeScript](https://www.typescriptlang.org/) and requires Node 12.x and yarn. Sources are located in [./lambdas/webhook]. +The Lambda function is written in [TypeScript](https://www.typescriptlang.org/) and requires Node and yarn. Sources are located in [./lambdas/webhook]. Check see `lambda.ts` for the different handler functions available. ### Install @@ -44,11 +44,13 @@ yarn run dist | Name | Version | |------|---------| | [aws](#provider\_aws) | ~> 5.27 | -| [null](#provider\_null) | ~> 3 | ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [direct](#module\_direct) | ./direct | n/a | +| [eventbridge](#module\_eventbridge) | ./eventbridge | n/a | ## Resources @@ -58,26 +60,14 @@ No modules. | [aws_apigatewayv2_integration.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_integration) | resource | | [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_cloudwatch_log_group.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | -| [aws_iam_role.webhook_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | -| [aws_iam_role_policy.webhook_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_role_policy.webhook_sqs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_role_policy.webhook_ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_role_policy.webhook_workflow_job_sqs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_role_policy.xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | -| [aws_iam_role_policy_attachment.webhook_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_lambda_function.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | -| [aws_lambda_permission.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | | [aws_ssm_parameter.runner_matcher_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | -| [null_resource.github_app_parameters](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | -| [aws_iam_policy_document.lambda_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [aws\_partition](#input\_aws\_partition) | (optional) partition for the base arn if not 'aws' | `string` | `"aws"` | no | +| [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.
key_base64 = string
id = string
webhook_secret = string
})
object({| n/a | yes | | [github\_app\_parameters](#input\_github\_app\_parameters) | Parameter Store for GitHub App Parameters. |
enable = optional(bool, false)
accept_events = optional(list(string), null)
})
object({| n/a | yes | | [kms\_key\_arn](#input\_kms\_key\_arn) | Optional CMK Key ARN to be used for Parameter Store. | `string` | `null` | no | | [lambda\_architecture](#input\_lambda\_architecture) | AWS Lambda architecture. Lambda functions using Graviton processors ('arm64') tend to have better price/performance than 'x86\_64' functions. | `string` | `"arm64"` | no | @@ -110,9 +100,12 @@ No modules. | Name | Description | |------|-------------| +| [dispatcher](#output\_dispatcher) | n/a | | [endpoint\_relative\_path](#output\_endpoint\_relative\_path) | n/a | +| [eventbridge](#output\_eventbridge) | n/a | | [gateway](#output\_gateway) | n/a | | [lambda](#output\_lambda) | n/a | | [lambda\_log\_group](#output\_lambda\_log\_group) | n/a | | [role](#output\_role) | n/a | +| [webhook](#output\_webhook) | n/a | diff --git a/modules/webhook/direct/README.md b/modules/webhook/direct/README.md new file mode 100644 index 0000000000..be9390c3dc --- /dev/null +++ b/modules/webhook/direct/README.md @@ -0,0 +1,52 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | ~> 5.27 | +| [null](#requirement\_null) | ~> 3.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 5.27 | +| [null](#provider\_null) | ~> 3.2 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_log_group.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_iam_role.webhook_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.webhook_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_sqs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_workflow_job_sqs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.webhook_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [null_resource.github_app_parameters](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.ssm_parameter_runner_matcher_config](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [aws_iam_policy_document.lambda_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | Configuration object for all variables. |
webhook_secret = map(string)
})
object({| n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [webhook](#output\_webhook) | n/a | +| [webhook\_lambda\_function](#output\_webhook\_lambda\_function) | n/a | + \ No newline at end of file diff --git a/modules/webhook/direct/main.tf b/modules/webhook/direct/main.tf new file mode 100644 index 0000000000..9937792f5e --- /dev/null +++ b/modules/webhook/direct/main.tf @@ -0,0 +1,6 @@ + +resource "null_resource" "ssm_parameter_runner_matcher_config" { + triggers = { + version = var.config.ssm_parameter_runner_matcher_config.version + } +} diff --git a/modules/webhook/direct/outputs.tf b/modules/webhook/direct/outputs.tf new file mode 100644 index 0000000000..891821c727 --- /dev/null +++ b/modules/webhook/direct/outputs.tf @@ -0,0 +1,12 @@ +output "webhook_lambda_function" { + value = aws_lambda_function.webhook +} + + +output "webhook" { + value = { + lambda = aws_lambda_function.webhook + log_group = aws_cloudwatch_log_group.webhook + role = aws_iam_role.webhook_lambda + } +} diff --git a/modules/webhook/policies.tf b/modules/webhook/direct/policies.tf similarity index 83% rename from modules/webhook/policies.tf rename to modules/webhook/direct/policies.tf index 454d943b4b..23bfea7d24 100644 --- a/modules/webhook/policies.tf +++ b/modules/webhook/direct/policies.tf @@ -1,5 +1,5 @@ data "aws_iam_policy_document" "lambda_xray" { - count = var.tracing_config.mode != null ? 1 : 0 + count = var.config.tracing_config.mode != null ? 1 : 0 statement { actions = [ "xray:BatchGetTraces", diff --git a/modules/webhook/direct/variables.tf b/modules/webhook/direct/variables.tf new file mode 100644 index 0000000000..dabad516a9 --- /dev/null +++ b/modules/webhook/direct/variables.tf @@ -0,0 +1,54 @@ +variable "config" { + description = "Configuration object for all variables." + type = object({ + prefix = string + archive = optional(object({ + enable = optional(bool, true) + retention_days = optional(number, 7) + }), {}) + tags = optional(map(string), {}) + + lambda_subnet_ids = optional(list(string), []) + lambda_security_group_ids = optional(list(string), []) + sqs_job_queues_arns = list(string) + sqs_workflow_job_queue = optional(object({ + id = string + arn = string + }), null) + lambda_zip = optional(string, null) + lambda_memory_size = optional(number, 256) + lambda_timeout = optional(number, 10) + role_permissions_boundary = optional(string, null) + role_path = optional(string, null) + logging_retention_in_days = optional(number, 180) + logging_kms_key_id = optional(string, null) + lambda_s3_bucket = optional(string, null) + lambda_s3_key = optional(string, null) + lambda_s3_object_version = optional(string, null) + lambda_apigateway_access_log_settings = optional(object({ + destination_arn = string + format = string + }), null) + repository_white_list = optional(list(string), []) + kms_key_arn = optional(string, null) + log_level = optional(string, "info") + lambda_runtime = optional(string, "nodejs20.x") + aws_partition = optional(string, "aws") + lambda_architecture = optional(string, "arm64") + github_app_parameters = object({ + webhook_secret = map(string) + }) + tracing_config = optional(object({ + mode = optional(string, null) + capture_http_requests = optional(bool, false) + capture_error = optional(bool, false) + }), {}) + lambda_tags = optional(map(string), {}) + api_gw_source_arn = string + ssm_parameter_runner_matcher_config = object({ + name = string + arn = string + version = string + }) + }) +} diff --git a/modules/webhook/direct/versions.tf b/modules/webhook/direct/versions.tf new file mode 100644 index 0000000000..d780c7775c --- /dev/null +++ b/modules/webhook/direct/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.27" + } + + null = { + source = "hashicorp/null" + version = "~> 3.2" + } + } +} diff --git a/modules/webhook/direct/webhook.tf b/modules/webhook/direct/webhook.tf new file mode 100644 index 0000000000..9fd24e3d30 --- /dev/null +++ b/modules/webhook/direct/webhook.tf @@ -0,0 +1,149 @@ +locals { + lambda_zip = var.config.lambda_zip == null ? "${path.module}/../../../lambdas/functions/webhook/webhook.zip" : var.config.lambda_zip +} + +resource "aws_lambda_function" "webhook" { + s3_bucket = var.config.lambda_s3_bucket != null ? var.config.lambda_s3_bucket : null + s3_key = var.config.lambda_s3_key != null ? var.config.lambda_s3_key : null + s3_object_version = var.config.lambda_s3_object_version != null ? var.config.lambda_s3_object_version : null + filename = var.config.lambda_s3_bucket == null ? local.lambda_zip : null + source_code_hash = var.config.lambda_s3_bucket == null ? filebase64sha256(local.lambda_zip) : null + function_name = "${var.config.prefix}-webhook" + role = aws_iam_role.webhook_lambda.arn + handler = "index.directWebhook" + runtime = var.config.lambda_runtime + memory_size = var.config.lambda_memory_size + timeout = var.config.lambda_timeout + architectures = [var.config.lambda_architecture] + + environment { + variables = { + for k, v in { + LOG_LEVEL = var.config.log_level + POWERTOOLS_LOGGER_LOG_EVENT = var.config.log_level == "debug" ? "true" : "false" + POWERTOOLS_TRACE_ENABLED = var.config.tracing_config.mode != null ? true : false + POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS = var.config.tracing_config.capture_http_requests + POWERTOOLS_TRACER_CAPTURE_ERROR = var.config.tracing_config.capture_error + PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.config.github_app_parameters.webhook_secret.name + REPOSITORY_ALLOW_LIST = jsonencode(var.config.repository_white_list) + SQS_WORKFLOW_JOB_QUEUE = try(var.config.sqs_workflow_job_queue.id, null) + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = var.config.ssm_parameter_runner_matcher_config.name + } : k => v if v != null + } + } + + dynamic "vpc_config" { + for_each = var.config.lambda_subnet_ids != null && var.config.lambda_security_group_ids != null ? [true] : [] + content { + security_group_ids = var.config.lambda_security_group_ids + subnet_ids = var.config.lambda_subnet_ids + } + } + + tags = merge(var.config.tags, var.config.lambda_tags) + + dynamic "tracing_config" { + for_each = var.config.tracing_config.mode != null ? [true] : [] + content { + mode = var.config.tracing_config.mode + } + } + + lifecycle { + replace_triggered_by = [null_resource.ssm_parameter_runner_matcher_config, null_resource.github_app_parameters] + } +} + +resource "aws_cloudwatch_log_group" "webhook" { + name = "/aws/lambda/${aws_lambda_function.webhook.function_name}" + retention_in_days = var.config.logging_retention_in_days + kms_key_id = var.config.logging_kms_key_id + tags = var.config.tags +} + +resource "aws_lambda_permission" "webhook" { + statement_id = "AllowExecutionFromAPIGateway" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.webhook.function_name + principal = "apigateway.amazonaws.com" + source_arn = var.config.api_gw_source_arn + lifecycle { + replace_triggered_by = [null_resource.ssm_parameter_runner_matcher_config, null_resource.github_app_parameters] + } +} + +resource "null_resource" "github_app_parameters" { + triggers = { + github_app_webhook_secret = var.config.github_app_parameters.webhook_secret.name + } +} + +data "aws_iam_policy_document" "lambda_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "webhook_lambda" { + name = "${var.config.prefix}-direct-webhook-lambda-role" + assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json + path = var.config.role_path + permissions_boundary = var.config.role_permissions_boundary + tags = var.config.tags +} + +resource "aws_iam_role_policy" "webhook_logging" { + name = "logging-policy" + role = aws_iam_role.webhook_lambda.name + policy = templatefile("${path.module}/../policies/lambda-cloudwatch.json", { + log_group_arn = aws_cloudwatch_log_group.webhook.arn + }) +} + +resource "aws_iam_role_policy_attachment" "webhook_vpc_execution_role" { + count = length(var.config.lambda_subnet_ids) > 0 ? 1 : 0 + role = aws_iam_role.webhook_lambda.name + policy_arn = "arn:${var.config.aws_partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" +} + +resource "aws_iam_role_policy" "webhook_sqs" { + name = "publish-sqs-policy" + role = aws_iam_role.webhook_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-publish-sqs-policy.json", { + sqs_resource_arns = jsonencode(var.config.sqs_job_queues_arns) + kms_key_arn = var.config.kms_key_arn != null ? var.config.kms_key_arn : "" + }) +} + +resource "aws_iam_role_policy" "webhook_workflow_job_sqs" { + count = var.config.sqs_workflow_job_queue != null ? 1 : 0 + name = "publish-workflow-job-sqs-policy" + role = aws_iam_role.webhook_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-publish-sqs-policy.json", { + sqs_resource_arns = jsonencode([var.config.sqs_workflow_job_queue.arn]) + kms_key_arn = var.config.kms_key_arn != null ? var.config.kms_key_arn : "" + }) +} + +resource "aws_iam_role_policy" "webhook_ssm" { + name = "publish-ssm-policy" + role = aws_iam_role.webhook_lambda.name + + policy = templatefile("${path.module}/../policies/lambda-ssm.json", { + resource_arns = jsonencode([var.config.github_app_parameters.webhook_secret.arn, var.config.ssm_parameter_runner_matcher_config.arn]) + }) +} + +resource "aws_iam_role_policy" "xray" { + count = var.config.tracing_config.mode != null ? 1 : 0 + name = "xray-policy" + policy = data.aws_iam_policy_document.lambda_xray[0].json + role = aws_iam_role.webhook_lambda.name +} diff --git a/modules/webhook/eventbridge/README.md b/modules/webhook/eventbridge/README.md new file mode 100644 index 0000000000..6426772d3d --- /dev/null +++ b/modules/webhook/eventbridge/README.md @@ -0,0 +1,65 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | ~> 5.0 | +| [null](#requirement\_null) | ~> 3.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | ~> 5.0 | +| [null](#provider\_null) | ~> 3.2 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudwatch_event_archive.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_archive) | resource | +| [aws_cloudwatch_event_bus.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_bus) | resource | +| [aws_cloudwatch_event_rule.workflow_job](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.github_welcome](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_cloudwatch_log_group.dispatcher](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_cloudwatch_log_group.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_iam_role.dispatcher_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.webhook_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy.dispatcher_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.dispatcher_sqs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.dispatcher_ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.dispatcher_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_logging](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.webhook_ssm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy.xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.dispatcher_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.webhook_vpc_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.dispatcher](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_function.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.allow_cloudwatch_to_call_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_lambda_permission.webhook](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [null_resource.github_app_parameters](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.ssm_parameter_runner_matcher_config](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [aws_iam_policy_document.lambda_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [config](#input\_config) | Configuration object for all variables. |
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})
lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
sqs_workflow_job_queue = optional(object({
id = string
arn = string
}), null)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
kms_key_arn = optional(string, null)
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs20.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = object({
name = string
arn = string
version = string
})
})
object({| n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [dispatcher](#output\_dispatcher) | n/a | +| [eventbridge](#output\_eventbridge) | n/a | +| [webhook](#output\_webhook) | n/a | + \ No newline at end of file diff --git a/modules/webhook/eventbridge/dispatcher.tf b/modules/webhook/eventbridge/dispatcher.tf new file mode 100644 index 0000000000..93d9af84e1 --- /dev/null +++ b/modules/webhook/eventbridge/dispatcher.tf @@ -0,0 +1,137 @@ +resource "aws_cloudwatch_event_rule" "workflow_job" { + name = "${var.config.prefix}-workflow_job" + description = "Workflow job event ruule for job queued." + event_bus_name = aws_cloudwatch_event_bus.main.name + + event_pattern = <
prefix = string
archive = optional(object({
enable = optional(bool, true)
retention_days = optional(number, 7)
}), {})
tags = optional(map(string), {})
lambda_subnet_ids = optional(list(string), [])
lambda_security_group_ids = optional(list(string), [])
sqs_job_queues_arns = list(string)
sqs_workflow_job_queue = optional(object({
id = string
arn = string
}), null)
lambda_zip = optional(string, null)
lambda_memory_size = optional(number, 256)
lambda_timeout = optional(number, 10)
role_permissions_boundary = optional(string, null)
role_path = optional(string, null)
logging_retention_in_days = optional(number, 180)
logging_kms_key_id = optional(string, null)
lambda_s3_bucket = optional(string, null)
lambda_s3_key = optional(string, null)
lambda_s3_object_version = optional(string, null)
lambda_apigateway_access_log_settings = optional(object({
destination_arn = string
format = string
}), null)
repository_white_list = optional(list(string), [])
kms_key_arn = optional(string, null)
log_level = optional(string, "info")
lambda_runtime = optional(string, "nodejs20.x")
aws_partition = optional(string, "aws")
lambda_architecture = optional(string, "arm64")
github_app_parameters = object({
webhook_secret = map(string)
})
tracing_config = optional(object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
}), {})
lambda_tags = optional(map(string), {})
api_gw_source_arn = string
ssm_parameter_runner_matcher_config = object({
name = string
arn = string
version = string
})
accept_events = optional(list(string), null)
})