From 1dd5618de6f841c0c7e6c4c84d12290e954a9f8b Mon Sep 17 00:00:00 2001 From: Javier Palomo Date: Tue, 28 Jul 2026 16:12:53 +0200 Subject: [PATCH] feat: support native SSM expiration policies for runner token parameters Registration tokens and JIT configs written to SSM are orphaned when an instance fails to boot or is terminated before registering. The only cleanup today is the scheduled SSM housekeeper lambda, which cannot delete parameters younger than a day, so orphaned parameters (billed hourly when on the Advanced tier) can live for up to ~2 days. The new opt-in `ssm_token_ttl_seconds` attaches a native SSM Expiration policy to the token / JIT config parameters so SSM deletes leftovers itself once the TTL passes. Parameter policies require the Advanced tier, which incurs additional cost per parameter, hence disabled by default. Expiration is enforced asynchronously by SSM and is a cleanup mechanism rather than a security boundary; the housekeeper lambda remains as a backstop. --- docs/configuration.md | 2 +- .../functions/control-plane/src/modules.d.ts | 1 + .../src/pool/pool-contract.test.ts | 3 +- .../control-plane/src/pool/pool.test.ts | 3 +- .../functions/control-plane/src/pool/pool.ts | 8 +++- .../src/scale-runners/github-runner.test.ts | 19 ++++++++ .../src/scale-runners/github-runner.ts | 13 ++++++ .../src/scale-runners/scale-up.ts | 3 ++ lambdas/libs/aws-ssm-util/src/index.test.ts | 46 +++++++++++++++++++ lambdas/libs/aws-ssm-util/src/index.ts | 21 ++++++++- .../ec2/src/control-plane/scale-up.test.ts | 41 +++++++++++++++++ lambdas/libs/runner-providers/core/index.ts | 1 + main.tf | 1 + modules/multi-runner/runners.tf | 1 + modules/multi-runner/variables.tf | 2 + modules/runners/pool.tf | 1 + modules/runners/pool/main.tf | 1 + modules/runners/pool/variables.tf | 1 + modules/runners/scale-up.tf | 1 + modules/runners/variables.tf | 11 +++++ variables.tf | 11 +++++ 21 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 lambdas/functions/control-plane/src/scale-runners/github-runner.test.ts diff --git a/docs/configuration.md b/docs/configuration.md index 155c131402..f814c1fb44 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -17,7 +17,7 @@ To be able to support a number of use-cases, the module has quite a lot of confi ## AWS SSM Parameters -The module uses the AWS System Manager Parameter Store to store configuration for the runners, as well as registration tokens and secrets for the Lambdas. Paths for the parameters can be configured via the variable `ssm_paths`. The location of the configuration parameters is retrieved by the runners via the instance tag `ghr:ssm_config_path`. The following default paths will be used. Tokens or JIT config stored in the token path will be deleted after retrieval by instance, data not deleted after a day will be deleted by a SSM housekeeper lambda. +The module uses the AWS System Manager Parameter Store to store configuration for the runners, as well as registration tokens and secrets for the Lambdas. Paths for the parameters can be configured via the variable `ssm_paths`. The location of the configuration parameters is retrieved by the runners via the instance tag `ghr:ssm_config_path`. The following default paths will be used. Tokens or JIT config stored in the token path will be deleted after retrieval by instance, data not deleted after a day will be deleted by a SSM housekeeper lambda. Alternatively you can set `ssm_token_ttl_seconds` to attach a native SSM expiration policy to the token / JIT config parameters so SSM deletes leftovers itself after the TTL passes. Be aware that parameter policies require the Advanced parameter tier for every token parameter, which incurs additional costs, and that expiration is enforced asynchronously by SSM. The housekeeper lambda remains active as a backstop. Furthermore, to accommodate larger JIT configurations or other stored values, the module implements automatic tier selection for SSM parameters: diff --git a/lambdas/functions/control-plane/src/modules.d.ts b/lambdas/functions/control-plane/src/modules.d.ts index 53247cf6c6..3a95cb9859 100644 --- a/lambdas/functions/control-plane/src/modules.d.ts +++ b/lambdas/functions/control-plane/src/modules.d.ts @@ -19,6 +19,7 @@ declare namespace NodeJS { RUNNER_PROVIDER_TYPE?: string; SCALE_DOWN_CONFIG: string; SSM_TOKEN_PATH: string; + SSM_TOKEN_TTL_SECONDS: string; SSM_CLEANUP_CONFIG: string; SUBNET_IDS: string; INSTANCE_TYPES: string; diff --git a/lambdas/functions/control-plane/src/pool/pool-contract.test.ts b/lambdas/functions/control-plane/src/pool/pool-contract.test.ts index e7c5eee21a..5792985eae 100644 --- a/lambdas/functions/control-plane/src/pool/pool-contract.test.ts +++ b/lambdas/functions/control-plane/src/pool/pool-contract.test.ts @@ -16,7 +16,8 @@ vi.mock('../github/auth', () => ({ createOctokitClient: vi.fn(), })); -vi.mock('../scale-runners/github-runner', () => ({ +vi.mock('../scale-runners/github-runner', async (importActual) => ({ + ...(await importActual()), createStartRunnerConfig: vi.fn(), getGitHubEnterpriseApiUrl: vi.fn(), validateSsmParameterStoreTags: vi.fn(), diff --git a/lambdas/functions/control-plane/src/pool/pool.test.ts b/lambdas/functions/control-plane/src/pool/pool.test.ts index 3aa194bbd4..f155b62d8c 100644 --- a/lambdas/functions/control-plane/src/pool/pool.test.ts +++ b/lambdas/functions/control-plane/src/pool/pool.test.ts @@ -42,7 +42,8 @@ vi.mock('@aws-github-runner/runner-providers/aws/ec2/control-plane/runner-config createRunners: vi.fn(), })); -vi.mock('../scale-runners/github-runner', async () => ({ +vi.mock('../scale-runners/github-runner', async (importActual) => ({ + ...(await importActual()), createStartRunnerConfig: vi.fn(), getGitHubEnterpriseApiUrl: vi.fn().mockReturnValue({ ghesApiUrl: '', diff --git a/lambdas/functions/control-plane/src/pool/pool.ts b/lambdas/functions/control-plane/src/pool/pool.ts index 3a6ed45be9..c6c5868d3e 100644 --- a/lambdas/functions/control-plane/src/pool/pool.ts +++ b/lambdas/functions/control-plane/src/pool/pool.ts @@ -5,7 +5,11 @@ import yn from 'yn'; import { createGithubAppAuth, createGithubInstallationAuth, createOctokitClient } from '../github/auth'; import { controlPlaneProviderRegistry } from '../control-plane-providers'; -import { getGitHubEnterpriseApiUrl, validateSsmParameterStoreTags } from '../scale-runners/github-runner'; +import { + getGitHubEnterpriseApiUrl, + parseSsmTokenTtlSeconds, + validateSsmParameterStoreTags, +} from '../scale-runners/github-runner'; import type { RunnerStatus } from './pool-provider'; const logger = createChildLogger('pool'); @@ -27,6 +31,7 @@ export async function adjust(event: PoolEvent): Promise { const runnerNamePrefix = process.env.RUNNER_NAME_PREFIX || ''; const environment = process.env.ENVIRONMENT; const ssmTokenPath = process.env.SSM_TOKEN_PATH; + const ssmTokenTtlSeconds = parseSsmTokenTtlSeconds(process.env.SSM_TOKEN_TTL_SECONDS); const ssmConfigPath = process.env.SSM_CONFIG_PATH || ''; const ephemeral = yn(process.env.ENABLE_EPHEMERAL_RUNNERS, { default: false }); const enableJitConfig = yn(process.env.ENABLE_JIT_CONFIG, { default: ephemeral }); @@ -93,6 +98,7 @@ export async function adjust(event: PoolEvent): Promise { runnerType: 'Org', disableAutoUpdate: disableAutoUpdate, ssmTokenPath, + ssmTokenTtlSeconds, ssmConfigPath, ssmParameterStoreTags, }, diff --git a/lambdas/functions/control-plane/src/scale-runners/github-runner.test.ts b/lambdas/functions/control-plane/src/scale-runners/github-runner.test.ts new file mode 100644 index 0000000000..607ecb6cce --- /dev/null +++ b/lambdas/functions/control-plane/src/scale-runners/github-runner.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; + +import { parseSsmTokenTtlSeconds } from './github-runner'; + +describe('parseSsmTokenTtlSeconds', () => { + it.each([ + [undefined, undefined], + ['', undefined], + [' ', undefined], + ['3600', 3600], + ['1', 1], + ])('parses %j to %j', (input, expected) => { + expect(parseSsmTokenTtlSeconds(input)).toBe(expected); + }); + + it.each([['not-a-number'], ['0'], ['-10']])('throws on invalid value %j', (input) => { + expect(() => parseSsmTokenTtlSeconds(input)).toThrow('SSM_TOKEN_TTL_SECONDS must be a positive number'); + }); +}); diff --git a/lambdas/functions/control-plane/src/scale-runners/github-runner.ts b/lambdas/functions/control-plane/src/scale-runners/github-runner.ts index cdc10b1f5d..363eed7f00 100644 --- a/lambdas/functions/control-plane/src/scale-runners/github-runner.ts +++ b/lambdas/functions/control-plane/src/scale-runners/github-runner.ts @@ -81,6 +81,17 @@ export function validateSsmParameterStoreTags(tagsJson: string): { Key: string; } } +export function parseSsmTokenTtlSeconds(ttl: string | undefined): number | undefined { + if (!ttl || ttl.trim() === '') { + return undefined; + } + const ttlSeconds = parseInt(ttl); + if (isNaN(ttlSeconds) || ttlSeconds <= 0) { + throw new Error(`SSM_TOKEN_TTL_SECONDS must be a positive number, got "${ttl}"`); + } + return ttlSeconds; +} + async function getGithubRunnerRegistrationToken(githubRunnerConfig: CreateGitHubRunnerConfig, ghClient: Octokit) { const registrationToken = githubRunnerConfig.runnerType === 'Org' @@ -270,6 +281,7 @@ async function createRegistrationTokenConfig( for (const runnerId of runnerIds) { await putParameter(`${githubRunnerConfig.ssmTokenPath}/${runnerId}`, runnerServiceConfig.join(' '), true, { tags: [...(options.getSsmParameterTags?.(runnerId) ?? []), ...githubRunnerConfig.ssmParameterStoreTags], + ttlSeconds: githubRunnerConfig.ssmTokenTtlSeconds, }); if (isDelay) { // Delay to prevent AWS ssm rate limits by being within the max throughput limit @@ -337,6 +349,7 @@ async function createJitConfig( }); await putParameter(`${githubRunnerConfig.ssmTokenPath}/${runnerId}`, runnerConfig.data.encoded_jit_config, true, { tags: [...(options.getSsmParameterTags?.(runnerId) ?? []), ...githubRunnerConfig.ssmParameterStoreTags], + ttlSeconds: githubRunnerConfig.ssmTokenTtlSeconds, }); if (isDelay) { // Delay to prevent AWS ssm rate limits by being within the max throughput limit diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-up.ts b/lambdas/functions/control-plane/src/scale-runners/scale-up.ts index cdb8b1bb4a..9edc88fbbe 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-up.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-up.ts @@ -11,6 +11,7 @@ import { resolveInstallationId, isJobQueued, UnsupportedEventError, + parseSsmTokenTtlSeconds, validateSsmParameterStoreTags, } from './github-runner'; import { publishRetryMessage } from './job-retry'; @@ -76,6 +77,7 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise { Tier: expectedTier, }); }); + + it('Puts parameters without an expiration policy when no TTL is given', async () => { + // Arrange + mockSSMClient.on(PutParameterCommand).resolves({ $metadata: { httpStatusCode: 200 } }); + + // Act + await putParameter('testParam', 'test', false); + + // Assert + expect(mockSSMClient).toHaveReceivedCommandWith(PutParameterCommand, { + Name: 'testParam', + Value: 'test', + Type: 'String', + Tier: 'Standard', + Policies: undefined, + }); + }); + + it('Puts parameters with an expiration policy and forces Advanced tier when a TTL is given', async () => { + // Arrange + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); + mockSSMClient.on(PutParameterCommand).resolves({ $metadata: { httpStatusCode: 200 } }); + + try { + // Act + await putParameter('testParam', 'test', true, { ttlSeconds: 3600 }); + + // Assert + expect(mockSSMClient).toHaveReceivedCommandWith(PutParameterCommand, { + Name: 'testParam', + Value: 'test', + Type: 'SecureString', + Tier: 'Advanced', + Policies: JSON.stringify([ + { + Type: 'Expiration', + Version: '1.0', + Attributes: { Timestamp: '2026-01-01T01:00:00.000Z' }, + }, + ]), + }); + } finally { + vi.useRealTimers(); + } + }); }); describe('Test getParameters (batch)', () => { diff --git a/lambdas/libs/aws-ssm-util/src/index.ts b/lambdas/libs/aws-ssm-util/src/index.ts index 71b33cbf41..75ba06517c 100644 --- a/lambdas/libs/aws-ssm-util/src/index.ts +++ b/lambdas/libs/aws-ssm-util/src/index.ts @@ -109,20 +109,37 @@ export async function putParameter( parameter_name: string, parameter_value: string, secure: boolean, - options: { tags?: Tag[] } = {}, + options: { tags?: Tag[]; ttlSeconds?: number } = {}, ): Promise { const client = ssmClient(); // Determine tier based on parameter_value size const valueSizeBytes = Buffer.byteLength(parameter_value, 'utf8'); + // Parameter policies (e.g. Expiration) are only supported on the Advanced + // tier, so a TTL forces the tier regardless of the value size. Expiration is + // enforced asynchronously by SSM: treat it as cleanup, not a security boundary. + const expiration = + options.ttlSeconds !== undefined + ? JSON.stringify([ + { + Type: 'Expiration', + Version: '1.0', + Attributes: { + Timestamp: new Date(Date.now() + options.ttlSeconds * 1000).toISOString(), + }, + }, + ]) + : undefined; + await client.send( new PutParameterCommand({ Name: parameter_name, Value: parameter_value, Type: secure ? 'SecureString' : 'String', Tags: options.tags, - Tier: valueSizeBytes >= SSM_ADVANCED_TIER_THRESHOLD ? 'Advanced' : 'Standard', + Tier: expiration || valueSizeBytes >= SSM_ADVANCED_TIER_THRESHOLD ? 'Advanced' : 'Standard', + Policies: expiration, }), ); } diff --git a/lambdas/libs/runner-providers/aws/ec2/src/control-plane/scale-up.test.ts b/lambdas/libs/runner-providers/aws/ec2/src/control-plane/scale-up.test.ts index 61287c89e5..9c4f07e7bf 100644 --- a/lambdas/libs/runner-providers/aws/ec2/src/control-plane/scale-up.test.ts +++ b/lambdas/libs/runner-providers/aws/ec2/src/control-plane/scale-up.test.ts @@ -455,6 +455,47 @@ describe('scaleUp with GHES', () => { }); }); + it('adds an expiration policy to the JIT config when SSM_TOKEN_TTL_SECONDS is set', async () => { + process.env.SSM_TOKEN_TTL_SECONDS = '3600'; + await scaleUpModule.scaleUp(TEST_DATA); + expect(mockSSMClient).toHaveReceivedNthSpecificCommandWith(1, PutParameterCommand, { + Name: '/github-action-runners/default/runners/config/i-12345', + Value: 'TEST_JIT_CONFIG_ORG', + Type: 'SecureString', + Tier: 'Advanced', + Policies: expect.stringContaining('"Type":"Expiration"') as unknown as string, + }); + }); + + it('adds an expiration policy to the registration token when SSM_TOKEN_TTL_SECONDS is set', async () => { + process.env.ENABLE_EPHEMERAL_RUNNERS = 'false'; + process.env.SSM_TOKEN_TTL_SECONDS = '3600'; + await scaleUpModule.scaleUp(TEST_DATA); + expect(mockOctokit.actions.createRegistrationTokenForOrg).toBeCalled(); + expect(mockSSMClient).toHaveReceivedNthSpecificCommandWith(1, PutParameterCommand, { + Name: '/github-action-runners/default/runners/config/i-12345', + Type: 'SecureString', + Tier: 'Advanced', + Policies: expect.stringContaining('"Type":"Expiration"') as unknown as string, + }); + }); + + it('does not add an expiration policy when SSM_TOKEN_TTL_SECONDS is not set', async () => { + await scaleUpModule.scaleUp(TEST_DATA); + expect(mockSSMClient).toHaveReceivedNthSpecificCommandWith(1, PutParameterCommand, { + Name: '/github-action-runners/default/runners/config/i-12345', + Value: 'TEST_JIT_CONFIG_ORG', + Type: 'SecureString', + Tier: 'Standard', + Policies: undefined, + }); + }); + + it('rejects an invalid SSM_TOKEN_TTL_SECONDS', async () => { + process.env.SSM_TOKEN_TTL_SECONDS = 'not-a-number'; + await expect(scaleUpModule.scaleUp(TEST_DATA)).rejects.toThrow('SSM_TOKEN_TTL_SECONDS must be a positive number'); + }); + it('quotes runner labels with semicolon separators in non-ephemeral runner config', async () => { process.env.ENABLE_EPHEMERAL_RUNNERS = 'false'; process.env.RUNNERS_MAXIMUM_COUNT = '2'; diff --git a/lambdas/libs/runner-providers/core/index.ts b/lambdas/libs/runner-providers/core/index.ts index e0e387acdf..25dafc7ec7 100644 --- a/lambdas/libs/runner-providers/core/index.ts +++ b/lambdas/libs/runner-providers/core/index.ts @@ -20,6 +20,7 @@ export interface CreateGitHubRunnerConfig { runnerType: GitHubRunnerType; disableAutoUpdate: boolean; ssmTokenPath: string; + ssmTokenTtlSeconds?: number; ssmConfigPath: string; ssmParameterStoreTags: { Key: string; Value: string }[]; } diff --git a/main.tf b/main.tf index ca83523285..f659e6083a 100644 --- a/main.tf +++ b/main.tf @@ -169,6 +169,7 @@ module "runners" { tokens = "${var.ssm_paths.runners}/tokens" config = "${var.ssm_paths.runners}/config" } + ssm_token_ttl_seconds = var.ssm_token_ttl_seconds s3_runner_binaries = var.enable_runner_binaries_syncer ? { arn = module.runner_binaries[0].bucket.arn diff --git a/modules/multi-runner/runners.tf b/modules/multi-runner/runners.tf index 892113dcc7..b5cca61efd 100644 --- a/modules/multi-runner/runners.tf +++ b/modules/multi-runner/runners.tf @@ -17,6 +17,7 @@ module "runners" { tokens = "${var.ssm_paths.runners}/tokens" config = "${var.ssm_paths.runners}/config" } + ssm_token_ttl_seconds = each.value.runner_config.ssm_token_ttl_seconds runner_os = each.value.runner_config.runner_os instance_types = each.value.runner_config.instance_types diff --git a/modules/multi-runner/variables.tf b/modules/multi-runner/variables.tf index df6fb77473..9a933e1511 100644 --- a/modules/multi-runner/variables.tf +++ b/modules/multi-runner/variables.tf @@ -182,6 +182,7 @@ variable "multi_runner_config" { schedule_expression_timezone = optional(string) size = number })), []) + ssm_token_ttl_seconds = optional(number, null) job_retry = optional(object({ enable = optional(bool, false) delay_in_seconds = optional(number, 300) @@ -279,6 +280,7 @@ variable "multi_runner_config" { block_device_mappings: "The EC2 instance block device configuration. Takes the following keys: `device_name`, `delete_on_termination`, `volume_type`, `volume_size`, `encrypted`, `iops`, `throughput`, `kms_key_id`, `snapshot_id`, `volume_initialization_rate`." job_retry: "Experimental! Can be removed / changed without trigger a major release. Configure job retries. The configuration enables job retries (for ephemeral runners). After creating the instances a message will be published to a job retry queue. The job retry check lambda is checking after a delay if the job is queued. If not the message will be published again on the scale-up (build queue). Using this feature can impact the rate limit of the GitHub app." pool_config: "The configuration for updating the pool. The `pool_size` to adjust to by the events triggered by the `schedule_expression`. For example you can configure a cron expression for week days to adjust the pool to 10 and another expression for the weekend to adjust the pool to 1. Use `schedule_expression_timezone` to override the schedule time zone (defaults to UTC)." + ssm_token_ttl_seconds: "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it." iam_overrides: "Allows to (optionally) override the instance profile and runner role created by the module. Set `override_instance_profile` to true and provide the `instance_profile_name` to use an existing instance profile. Set `override_runner_role` to true and provide the `runner_role_arn` to use an existing role for the runner instances." } matcherConfig: { diff --git a/modules/runners/pool.tf b/modules/runners/pool.tf index 11840a4638..10112adb6d 100644 --- a/modules/runners/pool.tf +++ b/modules/runners/pool.tf @@ -59,6 +59,7 @@ module "pool" { } subnet_ids = var.subnet_ids ssm_token_path = "${var.ssm_paths.root}/${var.ssm_paths.tokens}" + ssm_token_ttl_seconds = var.ssm_token_ttl_seconds ssm_config_path = "${var.ssm_paths.root}/${var.ssm_paths.config}" ami_id_ssm_parameter_name = local.ami_id_ssm_parameter_name ami_id_ssm_parameter_read_policy_arn = local.ami_id_ssm_parameter_name != null ? aws_iam_policy.ami_id_ssm_parameter_read[0].arn : null diff --git a/modules/runners/pool/main.tf b/modules/runners/pool/main.tf index e4f2485ccd..20e309155e 100644 --- a/modules/runners/pool/main.tf +++ b/modules/runners/pool/main.tf @@ -50,6 +50,7 @@ resource "aws_lambda_function" "pool" { RUNNER_OWNER = var.config.runner.pool_owner RUNNERS_MAXIMUM_COUNT = var.config.runners_maximum_count SSM_TOKEN_PATH = var.config.ssm_token_path + SSM_TOKEN_TTL_SECONDS = var.config.ssm_token_ttl_seconds != null ? var.config.ssm_token_ttl_seconds : "" SSM_CONFIG_PATH = var.config.ssm_config_path SUBNET_IDS = join(",", var.config.subnet_ids) POWERTOOLS_SERVICE_NAME = "${var.config.prefix}-pool" diff --git a/modules/runners/pool/variables.tf b/modules/runners/pool/variables.tf index adf5ad571c..4c3e5710ec 100644 --- a/modules/runners/pool/variables.tf +++ b/modules/runners/pool/variables.tf @@ -67,6 +67,7 @@ variable "config" { ami_id_ssm_parameter_arn = string role_path = string ssm_token_path = string + ssm_token_ttl_seconds = optional(number, null) ssm_config_path = string ami_id_ssm_parameter_name = string ami_id_ssm_parameter_read_policy_arn = string diff --git a/modules/runners/scale-up.tf b/modules/runners/scale-up.tf index bdda3c070f..d99b5088de 100644 --- a/modules/runners/scale-up.tf +++ b/modules/runners/scale-up.tf @@ -58,6 +58,7 @@ resource "aws_lambda_function" "scale_up" { RUNNERS_MAXIMUM_COUNT = var.runners_maximum_count POWERTOOLS_SERVICE_NAME = "${var.prefix}-scale-up" SSM_TOKEN_PATH = local.token_path + SSM_TOKEN_TTL_SECONDS = var.ssm_token_ttl_seconds != null ? var.ssm_token_ttl_seconds : "" SSM_CONFIG_PATH = "${var.ssm_paths.root}/${var.ssm_paths.config}" SSM_PARAMETER_STORE_TAGS = local.parameter_store_tags SUBNET_IDS = join(",", var.subnet_ids) diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index 08283ce65c..d0eb74ea36 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -654,6 +654,17 @@ variable "ssm_paths" { }) } +variable "ssm_token_ttl_seconds" { + description = "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it." + type = number + default = null + + validation { + condition = var.ssm_token_ttl_seconds == null ? true : var.ssm_token_ttl_seconds > 0 + error_message = "`ssm_token_ttl_seconds` must be a positive number." + } +} + variable "runner_name_prefix" { description = "The prefix used for the GitHub runner name. The prefix will be used in the default start script to prefix the instance name when register the runner in GitHub. The value is available via an EC2 tag 'ghr:runner_name_prefix'." type = string diff --git a/variables.tf b/variables.tf index 4af2ab4cd1..be731b44cd 100644 --- a/variables.tf +++ b/variables.tf @@ -935,6 +935,17 @@ variable "ssm_paths" { default = {} } +variable "ssm_token_ttl_seconds" { + description = "Optional TTL in seconds for the SSM parameters holding the runner registration token / JIT config. When set, the parameters are created with an SSM expiration policy so SSM deletes them itself after the TTL passes. Requires the Advanced parameter tier for every token parameter, which incurs additional costs. Expiration is enforced asynchronously by SSM; the SSM housekeeper lambda remains as a backstop. Must be a positive number, and should comfortably exceed the runner boot time so the config does not expire before the instance reads it." + type = number + default = null + + validation { + condition = var.ssm_token_ttl_seconds == null ? true : var.ssm_token_ttl_seconds > 0 + error_message = "`ssm_token_ttl_seconds` must be a positive number." + } +} + variable "runner_name_prefix" { description = "The prefix used for the GitHub runner name. The prefix will be used in the default start script to prefix the instance name when register the runner in GitHub. The value is available via an EC2 tag 'ghr:runner_name_prefix'." type = string