From 32492e3780509d4ff67d0ed3ad55f3506174974c Mon Sep 17 00:00:00 2001 From: Navdeep Gupta Date: Mon, 26 Feb 2024 07:28:51 -0500 Subject: [PATCH] feat: migrate webhook runner configuration to SSM (#3728) This PR migrates the confugration for the webhook from environment variables to SSM to avoid the maximum size of environment variables is reached. ## Implementation The webhook will read the configuration from SSM as json string. As long the lambda is hot the configuration is cached to speed-up the lambda time. In cases of configuration changes Lambda resources will be re-created by Terraform to ensure no cached values are used. fix: #3594 --------- Co-authored-by: github-actions[bot] Co-authored-by: Niek Palm Co-authored-by: Niek Palm --- README.md | 2 +- docs/configuration.md | 2 +- .../control-plane/src/lambda.test.ts | 1 + .../functions/webhook/src/ConfigResolver.ts | 46 +++++++++++---- lambdas/functions/webhook/src/lambda.test.ts | 6 ++ lambdas/functions/webhook/src/lambda.ts | 2 +- lambdas/functions/webhook/src/local.ts | 6 +- .../functions/webhook/src/sqs/index.test.ts | 13 ++++- lambdas/functions/webhook/src/sqs/index.ts | 4 +- .../webhook/src/webhook/index.test.ts | 58 +++++++++++-------- .../functions/webhook/src/webhook/index.ts | 13 ++--- main.tf | 7 ++- modules/multi-runner/README.md | 2 +- modules/multi-runner/variables.tf | 1 + modules/multi-runner/webhook.tf | 6 +- modules/runners/README.md | 2 +- modules/runners/variables.tf | 2 +- modules/webhook/README.md | 7 ++- modules/webhook/main.tf | 7 +++ modules/webhook/policies/lambda-ssm.json | 7 +-- modules/webhook/variables.tf | 14 ++++- modules/webhook/versions.tf | 5 ++ modules/webhook/webhook.tf | 23 ++++++-- variables.tf | 1 + 24 files changed, 164 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 2557717c17..ba1ab0a287 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ Talk to the forestkeepers in the `runners-channel` on Slack. | [runners\_ssm\_housekeeper](#input\_runners\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`enabled`: enable or disable the lambda trigger via the EventBridge.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
enabled = optional(bool, true)
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | | [scale\_down\_schedule\_expression](#input\_scale\_down\_schedule\_expression) | Scheduler expression to check every x for scale down. | `string` | `"cron(*/5 * * * ? *)"` | no | | [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | -| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
use_prefix = optional(bool, true)
})
| `{}` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
webhook = optional(string, "webhook")
use_prefix = optional(bool, true)
})
| `{}` | no | | [state\_event\_rule\_binaries\_syncer](#input\_state\_event\_rule\_binaries\_syncer) | Option to disable EventBridge Lambda trigger for the binary syncer, useful to stop automatic updates of binary distribution | `string` | `"ENABLED"` | no | | [subnet\_ids](#input\_subnet\_ids) | List of subnets in which the action runner instances will be launched. The subnets need to exist in the configured VPC (`vpc_id`), and must reside in different availability zones (see https://github.com/philips-labs/terraform-aws-github-runner/issues/2904) | `list(string)` | n/a | yes | | [syncer\_lambda\_s3\_key](#input\_syncer\_lambda\_s3\_key) | S3 key for syncer lambda function. Required if using an S3 bucket to specify lambdas. | `string` | `null` | no | diff --git a/docs/configuration.md b/docs/configuration.md index 44ac5ef394..29980fc68f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -22,7 +22,7 @@ The module uses the AWS System Manager Parameter Store to store configuration fo | `ssm_paths.root/var.prefix?/app/` | App secrets used by Lambda's | | `ssm_paths.root/var.prefix?/runners/config/` | Configuration parameters used by runner start script | | `ssm_paths.root/var.prefix?/runners/tokens/` | 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. | Available configuration parameters: | Parameter name | Description | diff --git a/lambdas/functions/control-plane/src/lambda.test.ts b/lambdas/functions/control-plane/src/lambda.test.ts index 9e2f5e0ce9..f4d28ccd79 100644 --- a/lambdas/functions/control-plane/src/lambda.test.ts +++ b/lambdas/functions/control-plane/src/lambda.test.ts @@ -64,6 +64,7 @@ jest.mock('./scale-runners/scale-down'); jest.mock('./pool/pool'); jest.mock('./scale-runners/ssm-housekeeper'); jest.mock('@terraform-aws-github-runner/aws-powertools-util'); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); // Docs for testing async with jest: https://jestjs.io/docs/tutorial-async describe('Test scale up lambda wrapper.', () => { diff --git a/lambdas/functions/webhook/src/ConfigResolver.ts b/lambdas/functions/webhook/src/ConfigResolver.ts index 2d72943e0c..15a4ed6589 100644 --- a/lambdas/functions/webhook/src/ConfigResolver.ts +++ b/lambdas/functions/webhook/src/ConfigResolver.ts @@ -1,15 +1,39 @@ -import { QueueConfig } from './sqs'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; +import { RunnerMatcherConfig } from './sqs'; +import { logger } from '@terraform-aws-github-runner/aws-powertools-util'; export class Config { - public repositoryAllowList: Array; - public queuesConfig: Array; - public workflowJobEventSecondaryQueue; - - constructor() { - const repositoryAllowListEnv = process.env.REPOSITORY_ALLOW_LIST || '[]'; - this.repositoryAllowList = JSON.parse(repositoryAllowListEnv) as Array; - const queuesConfigEnv = process.env.RUNNER_CONFIG || '[]'; - this.queuesConfig = JSON.parse(queuesConfigEnv) as Array; - this.workflowJobEventSecondaryQueue = process.env.SQS_WORKFLOW_JOB_QUEUE || undefined; + repositoryAllowList: Array; + static matcherConfig: Array | undefined; + static webhookSecret: string | undefined; + workflowJobEventSecondaryQueue: string | undefined; + + constructor(repositoryAllowList: Array, workflowJobEventSecondaryQueue: string | undefined) { + this.repositoryAllowList = repositoryAllowList; + + this.workflowJobEventSecondaryQueue = workflowJobEventSecondaryQueue; + } + + static async load(): Promise { + const repositoryAllowListEnv = process.env.REPOSITORY_ALLOW_LIST ?? '[]'; + const repositoryAllowList = JSON.parse(repositoryAllowListEnv) as Array; + // load parallel config if not cached + if (!Config.matcherConfig) { + const matcherConfigPath = + process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH ?? '/github-runner/runner-matcher-config'; + const [matcherConfigVal, webhookSecret] = await Promise.all([ + getParameter(matcherConfigPath), + getParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET), + ]); + Config.webhookSecret = webhookSecret; + Config.matcherConfig = JSON.parse(matcherConfigVal) as Array; + logger.debug('Loaded queues config', { matcherConfig: Config.matcherConfig }); + } + const workflowJobEventSecondaryQueue = process.env.SQS_WORKFLOW_JOB_QUEUE ?? undefined; + return new Config(repositoryAllowList, workflowJobEventSecondaryQueue); + } + + static reset(): void { + Config.matcherConfig = undefined; } } diff --git a/lambdas/functions/webhook/src/lambda.test.ts b/lambdas/functions/webhook/src/lambda.test.ts index 4b521140a4..6d1ed5da0f 100644 --- a/lambdas/functions/webhook/src/lambda.test.ts +++ b/lambdas/functions/webhook/src/lambda.test.ts @@ -5,6 +5,7 @@ import { mocked } from 'jest-mock'; import { githubWebhook } from './lambda'; import { handle } from './webhook'; import ValidationError from './ValidatonError'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; const event: APIGatewayEvent = { body: JSON.stringify(''), @@ -73,8 +74,13 @@ const context: Context = { }; jest.mock('./webhook'); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); describe('Test scale up lambda wrapper.', () => { + beforeEach(() => { + const mockedGet = mocked(getParameter); + mockedGet.mockResolvedValue('[]'); + }); it('Happy flow, resolve.', async () => { const mock = mocked(handle); mock.mockImplementation(() => { diff --git a/lambdas/functions/webhook/src/lambda.ts b/lambdas/functions/webhook/src/lambda.ts index 089d620599..4bc2e3c366 100644 --- a/lambdas/functions/webhook/src/lambda.ts +++ b/lambdas/functions/webhook/src/lambda.ts @@ -16,7 +16,7 @@ middy(githubWebhook).use(captureLambdaHandler(tracer)); export async function githubWebhook(event: APIGatewayEvent, context: Context): Promise { setContext(context, 'lambda.ts'); - const config = new Config(); + const config = await Config.load(); logger.logEventIfEnabled(event); logger.debug('Loading config', { config }); diff --git a/lambdas/functions/webhook/src/local.ts b/lambdas/functions/webhook/src/local.ts index b73062c967..ddedb552f4 100644 --- a/lambdas/functions/webhook/src/local.ts +++ b/lambdas/functions/webhook/src/local.ts @@ -5,12 +5,12 @@ import { handle } from './webhook'; import { Config } from './ConfigResolver'; const app = express(); -const config = new Config(); +const config = Config.load(); app.use(bodyParser.json()); -app.post('/event_handler', (req, res) => { - handle(req.headers, JSON.stringify(req.body), config) +app.post('/event_handler', async (req, res) => { + handle(req.headers, JSON.stringify(req.body), await config) .then((c) => res.status(c.statusCode).end()) .catch((e) => { console.log(e); diff --git a/lambdas/functions/webhook/src/sqs/index.test.ts b/lambdas/functions/webhook/src/sqs/index.test.ts index bda003a8da..c8b66adcb5 100644 --- a/lambdas/functions/webhook/src/sqs/index.test.ts +++ b/lambdas/functions/webhook/src/sqs/index.test.ts @@ -3,6 +3,8 @@ import { SendMessageCommandInput } from '@aws-sdk/client-sqs'; import { ActionRequestMessage, GithubWorkflowEvent, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '.'; import workflowjob_event from '../../test/resources/github_workflowjob_event.json'; import { Config } from '../ConfigResolver'; +import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; +import { mocked } from 'jest-mock'; const mockSQS = { sendMessage: jest.fn(() => { @@ -12,6 +14,7 @@ const mockSQS = { jest.mock('@aws-sdk/client-sqs', () => ({ SQS: jest.fn().mockImplementation(() => mockSQS), })); +jest.mock('@terraform-aws-github-runner/aws-ssm-util'); describe('Test sending message to SQS.', () => { const queueUrl = 'https://sqs.eu-west-1.amazonaws.com/123456789/queued-builds'; @@ -74,6 +77,10 @@ describe('Test sending message to SQS.', () => { QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/123456789/webhook_events_workflow_job_queue', MessageBody: JSON.stringify(message), }; + beforeEach(() => { + const mockedGet = mocked(getParameter); + mockedGet.mockResolvedValue('[]'); + }); afterEach(() => { jest.clearAllMocks(); }); @@ -81,7 +88,7 @@ describe('Test sending message to SQS.', () => { it('sends webhook events to workflow job queue', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl; - const config = new Config(); + const config = await Config.load(); // Act const result = await sendWebhookEventToWorkflowJobQueue(message, config); @@ -94,7 +101,7 @@ describe('Test sending message to SQS.', () => { it('Does not send webhook events to workflow job event copy queue', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = ''; - const config = new Config(); + const config = await Config.load(); // Act await sendWebhookEventToWorkflowJobQueue(message, config); @@ -105,7 +112,7 @@ describe('Test sending message to SQS.', () => { it('Catch the exception when even copy queue throws exception', async () => { // Arrange process.env.SQS_WORKFLOW_JOB_QUEUE = sqsMessage.QueueUrl; - const config = new Config(); + const config = await Config.load(); const mockSQS = { sendMessage: jest.fn(() => { diff --git a/lambdas/functions/webhook/src/sqs/index.ts b/lambdas/functions/webhook/src/sqs/index.ts index 7f506b374a..8a1e8e20af 100644 --- a/lambdas/functions/webhook/src/sqs/index.ts +++ b/lambdas/functions/webhook/src/sqs/index.ts @@ -20,9 +20,9 @@ export interface MatcherConfig { exactMatch: boolean; } -export type RunnerConfig = QueueConfig[]; +export type RunnerConfig = RunnerMatcherConfig[]; -export interface QueueConfig { +export interface RunnerMatcherConfig { matcherConfig: MatcherConfig; id: string; arn: string; diff --git a/lambdas/functions/webhook/src/webhook/index.test.ts b/lambdas/functions/webhook/src/webhook/index.test.ts index d4e73ecdc0..cacbde23f5 100644 --- a/lambdas/functions/webhook/src/webhook/index.test.ts +++ b/lambdas/functions/webhook/src/webhook/index.test.ts @@ -37,18 +37,17 @@ describe('handler', () => { let originalError: Console['error']; let config: Config; - beforeEach(() => { + beforeEach(async () => { process.env = { ...cleanEnv }; nock.disableNetConnect(); - config = new Config(); originalError = console.error; console.error = jest.fn(); jest.clearAllMocks(); jest.resetAllMocks(); - const mockedGet = mocked(getParameter); - mockedGet.mockResolvedValueOnce(GITHUB_APP_WEBHOOK_SECRET); + mockSSMResponse(); + config = await Config.load(); }); afterEach(() => { @@ -73,8 +72,8 @@ describe('handler', () => { }); describe('Test for workflowjob event: ', () => { - beforeEach(() => { - config = createConfig(undefined, runnerConfig); + beforeEach(async () => { + config = await createConfig(undefined, runnerConfig); }); it('handles workflow job events with 256 hash signature', async () => { @@ -122,7 +121,7 @@ describe('handler', () => { it('does not handle workflow_job events from unlisted repositories', async () => { const event = JSON.stringify(workflowjob_event); - config = createConfig(['NotCodertocat/Hello-World']); + config = await createConfig(['NotCodertocat/Hello-World']); await expect( handle({ 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, config), ).rejects.toMatchObject({ @@ -133,7 +132,7 @@ describe('handler', () => { it('handles workflow_job events without installation id', async () => { const event = JSON.stringify({ ...workflowjob_event, installation: null }); - config = createConfig(['philips-labs/terraform-aws-github-runner']); + config = await createConfig(['philips-labs/terraform-aws-github-runner']); const resp = await handle( { 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, @@ -145,7 +144,7 @@ describe('handler', () => { it('handles workflow_job events from allow listed repositories', async () => { const event = JSON.stringify(workflowjob_event); - config = createConfig(['philips-labs/terraform-aws-github-runner']); + config = await createConfig(['philips-labs/terraform-aws-github-runner']); const resp = await handle( { 'X-Hub-Signature-256': await webhooks.sign(event), 'X-GitHub-Event': 'workflow_job' }, event, @@ -156,7 +155,7 @@ describe('handler', () => { }); it('Check runner labels accept test job', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -189,7 +188,7 @@ describe('handler', () => { }); it('Check runner labels accept job with mixed order.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -222,7 +221,7 @@ describe('handler', () => { }); it('Check webhook accept jobs where not all labels are provided in job.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -255,7 +254,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where not all labels are supported (single matcher).', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -282,7 +281,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where the job labels are spread across label matchers.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -312,7 +311,7 @@ describe('handler', () => { }); it('Check webhook does not accept jobs where not all labels are supported by the runner.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -346,7 +345,7 @@ describe('handler', () => { }); it('Check webhook will accept jobs with a single acceptable label.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -380,7 +379,7 @@ describe('handler', () => { }); it('Check webhook will not accept jobs without correct label when job label check all is false.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -412,7 +411,7 @@ describe('handler', () => { expect(sendActionRequest).not.toBeCalled(); }); it('Check webhook will accept jobs for specific labels if workflow labels are specific', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -454,7 +453,7 @@ describe('handler', () => { }); }); it('Check webhook will accept jobs for latest labels if workflow labels are not specific', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -498,7 +497,7 @@ describe('handler', () => { }); it('Check webhook will accept jobs when matchers accepts multiple labels.', async () => { - config = createConfig(undefined, [ + config = await createConfig(undefined, [ { ...runnerConfig[0], matcherConfig: { @@ -599,12 +598,21 @@ describe('canRunJob', () => { }); }); -function createConfig(repositoryAllowList?: string[], runnerConfig?: RunnerConfig): Config { +async function createConfig(repositoryAllowList?: string[], runnerConfig?: RunnerConfig): Promise { if (repositoryAllowList) { process.env.REPOSITORY_ALLOW_LIST = JSON.stringify(repositoryAllowList); } - if (runnerConfig) { - process.env.RUNNER_CONFIG = JSON.stringify(runnerConfig); - } - return new Config(); + Config.reset(); + mockSSMResponse(runnerConfig); + return await Config.load(); +} +function mockSSMResponse(runnerConfigInput?: RunnerConfig) { + const mockedGet = mocked(getParameter); + mockedGet.mockImplementation((parameter_name) => { + const value = + parameter_name == '/github-runner/runner-matcher-config' + ? JSON.stringify(runnerConfigInput ?? runnerConfig) + : GITHUB_APP_WEBHOOK_SECRET; + return Promise.resolve(value); + }); } diff --git a/lambdas/functions/webhook/src/webhook/index.ts b/lambdas/functions/webhook/src/webhook/index.ts index 522f2dbd2e..92daf8fea2 100644 --- a/lambdas/functions/webhook/src/webhook/index.ts +++ b/lambdas/functions/webhook/src/webhook/index.ts @@ -1,11 +1,10 @@ import { Webhooks } from '@octokit/webhooks'; import { CheckRunEvent, WorkflowJobEvent } from '@octokit/webhooks-types'; import { createChildLogger } from '@terraform-aws-github-runner/aws-powertools-util'; -import { getParameter } from '@terraform-aws-github-runner/aws-ssm-util'; import { IncomingHttpHeaders } from 'http'; import { Response } from '../lambda'; -import { QueueConfig, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '../sqs'; +import { RunnerMatcherConfig, sendActionRequest, sendWebhookEventToWorkflowJobQueue } from '../sqs'; import ValidationError from '../ValidatonError'; import { Config } from '../ConfigResolver'; @@ -21,7 +20,7 @@ export async function handle(headers: IncomingHttpHeaders, body: string, config: validateRepoInAllowList(event, config); - const response = await handleWorkflowJob(event, eventType, config.queuesConfig); + const response = await handleWorkflowJob(event, eventType, Config.matcherConfig!); await sendWebhookEventToWorkflowJobQueue({ workflowJobEvent: event }, config); return response; } @@ -36,15 +35,15 @@ function validateRepoInAllowList(event: WorkflowJobEvent, config: Config) { async function handleWorkflowJob( body: WorkflowJobEvent, githubEvent: string, - queuesConfig: Array, + matcherConfig: Array, ): Promise { const installationId = getInstallationId(body); if (body.action === 'queued') { // sort the queuesConfig by order of matcher config exact match, with all true matches lined up ahead. - queuesConfig.sort((a, b) => { + matcherConfig.sort((a, b) => { return a.matcherConfig.exactMatch === b.matcherConfig.exactMatch ? 0 : a.matcherConfig.exactMatch ? -1 : 1; }); - for (const queue of queuesConfig) { + for (const queue of matcherConfig) { if (canRunJob(body.workflow_job.labels, queue.matcherConfig.labelMatchers, queue.matcherConfig.exactMatch)) { await sendActionRequest({ id: body.workflow_job.id, @@ -96,7 +95,7 @@ export function canRunJob( async function verifySignature(headers: IncomingHttpHeaders, body: string): Promise { const signature = headers['x-hub-signature-256'] as string; const webhooks = new Webhooks({ - secret: await getParameter(process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET), + secret: Config.webhookSecret!, }); if ( diff --git a/main.tf b/main.tf index fc576fdf8a..4e9b7d0f26 100644 --- a/main.tf +++ b/main.tf @@ -123,12 +123,15 @@ module "ssm" { module "webhook" { source = "./modules/webhook" - + ssm_paths = { + root = "${local.ssm_root_path}" + webhook = "${var.ssm_paths.webhook}" + } prefix = var.prefix tags = local.tags kms_key_arn = var.kms_key_arn - runner_config = { + runner_matcher_config = { (aws_sqs_queue.queued_builds.id) = { id : aws_sqs_queue.queued_builds.id arn : aws_sqs_queue.queued_builds.arn diff --git a/modules/multi-runner/README.md b/modules/multi-runner/README.md index 84b1c9d4e0..9bf645cc0d 100644 --- a/modules/multi-runner/README.md +++ b/modules/multi-runner/README.md @@ -163,7 +163,7 @@ module "multi-runner" { | [runners\_scale\_down\_lambda\_timeout](#input\_runners\_scale\_down\_lambda\_timeout) | Time out for the scale down lambda in seconds. | `number` | `60` | no | | [runners\_scale\_up\_lambda\_timeout](#input\_runners\_scale\_up\_lambda\_timeout) | Time out for the scale up lambda in seconds. | `number` | `30` | no | | [runners\_ssm\_housekeeper](#input\_runners\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`enabled`: enable or disable the lambda trigger via the EventBridge.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
enabled = optional(bool, true)
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | -| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secreets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
})
| `{}` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secreets. |
object({
root = optional(string, "github-action-runners")
app = optional(string, "app")
runners = optional(string, "runners")
webhook = optional(string, "webhook")
})
| `{}` | no | | [state\_event\_rule\_binaries\_syncer](#input\_state\_event\_rule\_binaries\_syncer) | Option to disable EventBridge Lambda trigger for the binary syncer, useful to stop automatic updates of binary distribution | `string` | `"ENABLED"` | no | | [subnet\_ids](#input\_subnet\_ids) | List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | n/a | yes | | [syncer\_lambda\_s3\_key](#input\_syncer\_lambda\_s3\_key) | S3 key for syncer lambda function. Required if using S3 bucket to specify lambdas. | `string` | `null` | no | diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index 01dfd6cd46..52d39b145a 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -542,6 +542,7 @@ variable "ssm_paths" { root = optional(string, "github-action-runners") app = optional(string, "app") runners = optional(string, "runners") + webhook = optional(string, "webhook") }) default = {} } diff --git a/modules/multi-runner/webhook.tf b/modules/multi-runner/webhook.tf index 9a1df4dce6..54ca3ef88d 100644 --- a/modules/multi-runner/webhook.tf +++ b/modules/multi-runner/webhook.tf @@ -4,7 +4,11 @@ module "webhook" { tags = local.tags kms_key_arn = var.kms_key_arn - runner_config = local.runner_config + runner_matcher_config = local.runner_config + ssm_paths = { + root = local.ssm_root_path + webhook = var.ssm_paths.webhook + } sqs_workflow_job_queue = length(aws_sqs_queue.webhook_events_workflow_job_queue) > 0 ? aws_sqs_queue.webhook_events_workflow_job_queue[0] : null github_app_parameters = { diff --git a/modules/runners/README.md b/modules/runners/README.md index 466df88e51..5665af0cae 100644 --- a/modules/runners/README.md +++ b/modules/runners/README.md @@ -207,7 +207,7 @@ yarn run dist | [scale\_up\_reserved\_concurrent\_executions](#input\_scale\_up\_reserved\_concurrent\_executions) | Amount of reserved concurrent executions for the scale-up lambda function. A value of 0 disables lambda from being triggered and -1 removes any concurrency limitations. | `number` | `1` | no | | [sqs\_build\_queue](#input\_sqs\_build\_queue) | SQS queue to consume accepted build events. |
object({
arn = string
})
| n/a | yes | | [ssm\_housekeeper](#input\_ssm\_housekeeper) | Configuration for the SSM housekeeper lambda. This lambda deletes token / JIT config from SSM.

`schedule_expression`: is used to configure the schedule for the lambda.
`state`: state of the cloudwatch event rule. Valid values are `DISABLED`, `ENABLED`, and `ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS`.
`lambda_timeout`: timeout for the lambda in seconds.
`config`: configuration for the lambda function. Token path will be read by default from the module. |
object({
schedule_expression = optional(string, "rate(1 day)")
state = optional(string, "ENABLED")
lambda_timeout = optional(number, 60)
config = object({
tokenPath = optional(string)
minimumDaysOld = optional(number, 1)
dryRun = optional(bool, false)
})
})
|
{
"config": {}
}
| no | -| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secreets. |
object({
root = string
tokens = string
config = string
})
| n/a | yes | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = string
tokens = string
config = string
})
| n/a | yes | | [subnet\_ids](#input\_subnet\_ids) | List of subnets in which the action runners will be launched, the subnets needs to be subnets in the `vpc_id`. | `list(string)` | n/a | yes | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index 2cca797a80..c67b0aab84 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -556,7 +556,7 @@ variable "enable_user_data_debug_logging" { } variable "ssm_paths" { - description = "The root path used in SSM to store configuration and secreets." + description = "The root path used in SSM to store configuration and secrets." type = object({ root = string tokens = string diff --git a/modules/webhook/README.md b/modules/webhook/README.md index 28c28cb48c..7c4d5bf474 100644 --- a/modules/webhook/README.md +++ b/modules/webhook/README.md @@ -38,12 +38,14 @@ yarn run dist |------|---------| | [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 @@ -67,6 +69,8 @@ No modules. | [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 | @@ -91,8 +95,9 @@ No modules. | [repository\_white\_list](#input\_repository\_white\_list) | List of github repository full names (owner/repo\_name) that will be allowed to use the github app. Leave empty for no filtering. | `list(string)` | `[]` | no | | [role\_path](#input\_role\_path) | The path that will be added to the role; if not set, the environment name will be used. | `string` | `null` | no | | [role\_permissions\_boundary](#input\_role\_permissions\_boundary) | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no | -| [runner\_config](#input\_runner\_config) | SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accecpts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. |
map(object({
arn = string
id = string
fifo = bool
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
priority = optional(number, 999)
})
}))
| n/a | yes | +| [runner\_matcher\_config](#input\_runner\_matcher\_config) | SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied. |
map(object({
arn = string
id = string
fifo = bool
matcherConfig = object({
labelMatchers = list(list(string))
exactMatch = bool
priority = optional(number, 999)
})
}))
| n/a | yes | | [sqs\_workflow\_job\_queue](#input\_sqs\_workflow\_job\_queue) | SQS queue to monitor github events. |
object({
id = string
arn = string
})
| `null` | no | +| [ssm\_paths](#input\_ssm\_paths) | The root path used in SSM to store configuration and secrets. |
object({
root = string
webhook = string
})
| n/a | yes | | [tags](#input\_tags) | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no | | [tracing\_config](#input\_tracing\_config) | Configuration for lambda tracing. |
object({
mode = optional(string, null)
capture_http_requests = optional(bool, false)
capture_error = optional(bool, false)
})
| `{}` | no | | [webhook\_lambda\_apigateway\_access\_log\_settings](#input\_webhook\_lambda\_apigateway\_access\_log\_settings) | Access log settings for webhook API gateway. |
object({
destination_arn = string
format = string
})
| `null` | no | diff --git a/modules/webhook/main.tf b/modules/webhook/main.tf index c49957efab..503332b563 100644 --- a/modules/webhook/main.tf +++ b/modules/webhook/main.tf @@ -55,3 +55,10 @@ resource "aws_apigatewayv2_integration" "webhook" { integration_method = "POST" integration_uri = aws_lambda_function.webhook.invoke_arn } + + +resource "aws_ssm_parameter" "runner_matcher_config" { + name = "${var.ssm_paths.root}/${var.ssm_paths.webhook}/runner-matcher-config" + type = "String" + value = jsonencode(local.runner_matcher_config_sorted) +} diff --git a/modules/webhook/policies/lambda-ssm.json b/modules/webhook/policies/lambda-ssm.json index efef0b907d..23864db305 100644 --- a/modules/webhook/policies/lambda-ssm.json +++ b/modules/webhook/policies/lambda-ssm.json @@ -3,11 +3,10 @@ "Statement": [ { "Effect": "Allow", - "Action": [ - "ssm:GetParameter" - ], + "Action": ["ssm:GetParameter"], "Resource": [ - "${github_app_webhook_secret_arn}" + "${github_app_webhook_secret_arn}", + "${parameter_runner_matcher_config_arn}" ] } ] diff --git a/modules/webhook/variables.tf b/modules/webhook/variables.tf index 2926d33d15..1ccabd188b 100644 --- a/modules/webhook/variables.tf +++ b/modules/webhook/variables.tf @@ -22,8 +22,8 @@ variable "tags" { default = {} } -variable "runner_config" { - description = "SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accecpts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied." +variable "runner_matcher_config" { + description = "SQS queue to publish accepted build events based on the runner type. When exact match is disabled the webhook accepts the event if one of the workflow job labels is part of the matcher. The priority defines the order the matchers are applied." type = map(object({ arn = string id = string @@ -35,7 +35,7 @@ variable "runner_config" { }) })) validation { - condition = try(var.runner_config.matcherConfig.priority, 999) >= 0 && try(var.runner_config.matcherConfig.priority, 999) < 1000 + condition = try(var.runner_matcher_config.matcherConfig.priority, 999) >= 0 && try(var.runner_matcher_config.matcherConfig.priority, 999) < 1000 error_message = "The priority of the matcher must be between 0 and 999." } } @@ -180,3 +180,11 @@ variable "tracing_config" { }) default = {} } + +variable "ssm_paths" { + description = "The root path used in SSM to store configuration and secrets." + type = object({ + root = string + webhook = string + }) +} diff --git a/modules/webhook/versions.tf b/modules/webhook/versions.tf index 1df1926c45..d780c7775c 100644 --- a/modules/webhook/versions.tf +++ b/modules/webhook/versions.tf @@ -6,5 +6,10 @@ terraform { source = "hashicorp/aws" version = "~> 5.27" } + + null = { + source = "hashicorp/null" + version = "~> 3.2" + } } } diff --git a/modules/webhook/webhook.tf b/modules/webhook/webhook.tf index 959a640e28..6b9ba75d1e 100644 --- a/modules/webhook/webhook.tf +++ b/modules/webhook/webhook.tf @@ -1,9 +1,9 @@ locals { # config with combined key and order - runner_config = { for k, v in var.runner_config : format("%03d-%s", v.matcherConfig.priority, k) => merge(v, { key = k }) } + runner_matcher_config = { for k, v in var.runner_matcher_config : format("%03d-%s", v.matcherConfig.priority, k) => merge(v, { key = k }) } # sorted list - runner_config_sorted = [for k in sort(keys(local.runner_config)) : local.runner_config[k]] + runner_matcher_config_sorted = [for k in sort(keys(local.runner_matcher_config)) : local.runner_matcher_config[k]] } @@ -29,8 +29,8 @@ resource "aws_lambda_function" "webhook" { POWERTOOLS_TRACER_CAPTURE_ERROR = var.tracing_config.capture_error PARAMETER_GITHUB_APP_WEBHOOK_SECRET = var.github_app_parameters.webhook_secret.name REPOSITORY_WHITE_LIST = jsonencode(var.repository_white_list) - RUNNER_CONFIG = jsonencode(local.runner_config_sorted) SQS_WORKFLOW_JOB_QUEUE = try(var.sqs_workflow_job_queue, null) != null ? var.sqs_workflow_job_queue.id : "" + PARAMETER_RUNNER_MATCHER_CONFIG_PATH = aws_ssm_parameter.runner_matcher_config.name } } @@ -50,6 +50,9 @@ resource "aws_lambda_function" "webhook" { mode = var.tracing_config.mode } } + lifecycle { + replace_triggered_by = [aws_ssm_parameter.runner_matcher_config, null_resource.github_app_parameters] + } } resource "aws_cloudwatch_log_group" "webhook" { @@ -65,6 +68,15 @@ resource "aws_lambda_permission" "webhook" { function_name = aws_lambda_function.webhook.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_apigatewayv2_api.webhook.execution_arn}/*/*/${local.webhook_endpoint}" + lifecycle { + replace_triggered_by = [aws_ssm_parameter.runner_matcher_config, null_resource.github_app_parameters] + } +} + +resource "null_resource" "github_app_parameters" { + triggers = { + github_app_webhook_secret = var.github_app_parameters.webhook_secret.name + } } data "aws_iam_policy_document" "lambda_assume_role_policy" { @@ -105,7 +117,7 @@ resource "aws_iam_role_policy" "webhook_sqs" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/policies/lambda-publish-sqs-policy.json", { - sqs_resource_arns = jsonencode([for k, v in var.runner_config : v.arn]) + sqs_resource_arns = jsonencode([for k, v in var.runner_matcher_config : v.arn]) kms_key_arn = var.kms_key_arn != null ? var.kms_key_arn : "" }) } @@ -126,7 +138,8 @@ resource "aws_iam_role_policy" "webhook_ssm" { role = aws_iam_role.webhook_lambda.name policy = templatefile("${path.module}/policies/lambda-ssm.json", { - github_app_webhook_secret_arn = var.github_app_parameters.webhook_secret.arn, + github_app_webhook_secret_arn = var.github_app_parameters.webhook_secret.arn, + parameter_runner_matcher_config_arn = aws_ssm_parameter.runner_matcher_config.arn }) } diff --git a/variables.tf b/variables.tf index d42115dd26..27f250cc0a 100644 --- a/variables.tf +++ b/variables.tf @@ -743,6 +743,7 @@ variable "ssm_paths" { root = optional(string, "github-action-runners") app = optional(string, "app") runners = optional(string, "runners") + webhook = optional(string, "webhook") use_prefix = optional(bool, true) }) default = {}