From eb7f6f8aaab1ff3cb894afa050e6fa1b7a6a3c21 Mon Sep 17 00:00:00 2001 From: s1v4-d <161426787+s1v4-d@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:51:23 +0000 Subject: [PATCH 1/2] feat(scale-up): add runner count caching to reduce EC2 API rate limiting (#4710) Implements a multi-tier caching strategy to address EC2 DescribeInstances API rate limiting in high-volume environments (20K+ runners/day): - In-memory TTL cache (5s) for within-invocation deduplication - DynamoDB-based persistent cache with EventBridge for cross-invocation consistency using EC2 state change events - Atomic counters for accurate runner count tracking - Feature is opt-in via `runner_count_cache = { enable = true }` This can reduce EC2 API calls by 90%+ and eliminate 15+ second latency spikes caused by DescribeInstances throttling. Closes #4710 Signed-off-by: s1v4-d <161426787+s1v4-d@users.noreply.github.com> --- lambdas/functions/control-plane/package.json | 1 + .../src/scale-runners/cache.test.ts | 239 +++++ .../control-plane/src/scale-runners/cache.ts | 231 +++++ .../src/scale-runners/scale-up.test.ts | 5 + .../src/scale-runners/scale-up.ts | 57 +- .../functions/runner-count-cache/package.json | 40 + .../runner-count-cache/src/lambda.ts | 176 ++++ .../runner-count-cache/tsconfig.json | 9 + .../runner-count-cache/vitest.config.ts | 20 + lambdas/yarn.lock | 890 ++++++++++++++++++ main.tf | 34 + modules/runner-count-cache/README.md | 98 ++ modules/runner-count-cache/lambda.tf | 149 +++ modules/runner-count-cache/main.tf | 79 ++ modules/runner-count-cache/outputs.tf | 39 + modules/runner-count-cache/variables.tf | 127 +++ modules/runner-count-cache/versions.tf | 10 + modules/runners/scale-up.tf | 22 + modules/runners/variables.tf | 15 + variables.runner-count-cache.tf | 28 + 20 files changed, 2267 insertions(+), 2 deletions(-) create mode 100644 lambdas/functions/control-plane/src/scale-runners/cache.test.ts create mode 100644 lambdas/functions/runner-count-cache/package.json create mode 100644 lambdas/functions/runner-count-cache/src/lambda.ts create mode 100644 lambdas/functions/runner-count-cache/tsconfig.json create mode 100644 lambdas/functions/runner-count-cache/vitest.config.ts create mode 100644 modules/runner-count-cache/README.md create mode 100644 modules/runner-count-cache/lambda.tf create mode 100644 modules/runner-count-cache/main.tf create mode 100644 modules/runner-count-cache/outputs.tf create mode 100644 modules/runner-count-cache/variables.tf create mode 100644 modules/runner-count-cache/versions.tf create mode 100644 variables.runner-count-cache.tf diff --git a/lambdas/functions/control-plane/package.json b/lambdas/functions/control-plane/package.json index 5bbba35155..f51f77b11d 100644 --- a/lambdas/functions/control-plane/package.json +++ b/lambdas/functions/control-plane/package.json @@ -33,6 +33,7 @@ "@aws-github-runner/aws-powertools-util": "*", "@aws-github-runner/aws-ssm-util": "*", "@aws-lambda-powertools/parameters": "^2.29.0", + "@aws-sdk/client-dynamodb": "^3.948.0", "@aws-sdk/client-ec2": "^3.948.0", "@aws-sdk/client-sqs": "^3.948.0", "@middy/core": "^6.4.5", diff --git a/lambdas/functions/control-plane/src/scale-runners/cache.test.ts b/lambdas/functions/control-plane/src/scale-runners/cache.test.ts new file mode 100644 index 0000000000..d2be08f6dc --- /dev/null +++ b/lambdas/functions/control-plane/src/scale-runners/cache.test.ts @@ -0,0 +1,239 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { ec2RunnerCountCache, dynamoDbRunnerCountCache } from './cache'; +import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'; +import { mockClient } from 'aws-sdk-client-mock'; + +const mockDynamoDBClient = mockClient(DynamoDBClient); + +describe('ec2RunnerCountCache', () => { + beforeEach(() => { + ec2RunnerCountCache.reset(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('get', () => { + it('should return undefined when cache is empty', () => { + const result = ec2RunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBeUndefined(); + }); + + it('should return cached value when within TTL', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + + // Advance time by 3 seconds (within default 5s TTL) + vi.advanceTimersByTime(3000); + + const result = ec2RunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBe(10); + }); + + it('should return undefined when cache entry is expired', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + + // Advance time by 6 seconds (past default 5s TTL) + vi.advanceTimersByTime(6000); + + const result = ec2RunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBeUndefined(); + }); + + it('should respect custom TTL', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + + // Advance time by 8 seconds + vi.advanceTimersByTime(8000); + + // Should be expired with default TTL but valid with custom 10s TTL + const expiredResult = ec2RunnerCountCache.get('prod', 'Org', 'my-org', 5000); + expect(expiredResult).toBeUndefined(); + + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 15); + vi.advanceTimersByTime(8000); + + const validResult = ec2RunnerCountCache.get('prod', 'Org', 'my-org', 10000); + expect(validResult).toBe(15); + }); + + it('should return different values for different keys', () => { + ec2RunnerCountCache.set('prod', 'Org', 'org-a', 10); + ec2RunnerCountCache.set('prod', 'Org', 'org-b', 20); + ec2RunnerCountCache.set('prod', 'Repo', 'owner/repo', 5); + + expect(ec2RunnerCountCache.get('prod', 'Org', 'org-a')).toBe(10); + expect(ec2RunnerCountCache.get('prod', 'Org', 'org-b')).toBe(20); + expect(ec2RunnerCountCache.get('prod', 'Repo', 'owner/repo')).toBe(5); + }); + }); + + describe('set', () => { + it('should store value in cache', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBe(10); + }); + + it('should overwrite existing value', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 20); + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBe(20); + }); + }); + + describe('increment', () => { + it('should increment existing cached value', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + ec2RunnerCountCache.increment('prod', 'Org', 'my-org', 5); + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBe(15); + }); + + it('should handle negative increments (decrement)', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + ec2RunnerCountCache.increment('prod', 'Org', 'my-org', -3); + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBe(7); + }); + + it('should do nothing if cache entry does not exist', () => { + ec2RunnerCountCache.increment('prod', 'Org', 'my-org', 5); + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBeUndefined(); + }); + + it('should reset TTL on increment', () => { + ec2RunnerCountCache.set('prod', 'Org', 'my-org', 10); + + // Advance time by 4 seconds + vi.advanceTimersByTime(4000); + + // Increment, which should reset the TTL + ec2RunnerCountCache.increment('prod', 'Org', 'my-org', 1); + + // Advance another 4 seconds (total 8 seconds from original set, but only 4 from increment) + vi.advanceTimersByTime(4000); + + // Should still be valid because TTL was reset + expect(ec2RunnerCountCache.get('prod', 'Org', 'my-org')).toBe(11); + }); + }); + + describe('reset', () => { + it('should clear all cache entries', () => { + ec2RunnerCountCache.set('prod', 'Org', 'org-a', 10); + ec2RunnerCountCache.set('prod', 'Org', 'org-b', 20); + + expect(ec2RunnerCountCache.size()).toBe(2); + + ec2RunnerCountCache.reset(); + + expect(ec2RunnerCountCache.size()).toBe(0); + expect(ec2RunnerCountCache.get('prod', 'Org', 'org-a')).toBeUndefined(); + }); + }); + + describe('size', () => { + it('should return correct cache size', () => { + expect(ec2RunnerCountCache.size()).toBe(0); + + ec2RunnerCountCache.set('prod', 'Org', 'org-a', 10); + expect(ec2RunnerCountCache.size()).toBe(1); + + ec2RunnerCountCache.set('prod', 'Org', 'org-b', 20); + expect(ec2RunnerCountCache.size()).toBe(2); + }); + }); +}); + +describe('dynamoDbRunnerCountCache', () => { + beforeEach(() => { + dynamoDbRunnerCountCache.reset(); + mockDynamoDBClient.reset(); + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe('isEnabled', () => { + it('should return false when not initialized', () => { + expect(dynamoDbRunnerCountCache.isEnabled()).toBe(false); + }); + + it('should return true after initialization', () => { + dynamoDbRunnerCountCache.initialize('test-table', 'us-east-1', 60000); + expect(dynamoDbRunnerCountCache.isEnabled()).toBe(true); + }); + }); + + describe('get', () => { + beforeEach(() => { + dynamoDbRunnerCountCache.initialize('test-table', 'us-east-1', 60000); + }); + + it('should return null when item not found in DynamoDB', async () => { + mockDynamoDBClient.on(GetItemCommand).resolves({ + Item: undefined, + }); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBeNull(); + }); + + it('should return count and isStale=false when item is fresh', async () => { + const now = Date.now(); + mockDynamoDBClient.on(GetItemCommand).resolves({ + Item: { + pk: { S: 'prod#Org#my-org' }, + count: { N: '10' }, + updated: { N: String(now - 30000) }, // 30 seconds ago + }, + }); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toEqual({ count: 10, isStale: false }); + }); + + it('should return count and isStale=true when item is stale', async () => { + const now = Date.now(); + mockDynamoDBClient.on(GetItemCommand).resolves({ + Item: { + pk: { S: 'prod#Org#my-org' }, + count: { N: '10' }, + updated: { N: String(now - 120000) }, // 2 minutes ago + }, + }); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toEqual({ count: 10, isStale: true }); + }); + + it('should return count >= 0 even if DynamoDB count is negative', async () => { + const now = Date.now(); + mockDynamoDBClient.on(GetItemCommand).resolves({ + Item: { + pk: { S: 'prod#Org#my-org' }, + count: { N: '-5' }, // Negative count due to race conditions + updated: { N: String(now) }, + }, + }); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toEqual({ count: 0, isStale: false }); + }); + + it('should return null on DynamoDB error', async () => { + mockDynamoDBClient.on(GetItemCommand).rejects(new Error('DynamoDB error')); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBeNull(); + }); + + it('should return null when not enabled', async () => { + dynamoDbRunnerCountCache.reset(); + + const result = await dynamoDbRunnerCountCache.get('prod', 'Org', 'my-org'); + expect(result).toBeNull(); + }); + }); +}); diff --git a/lambdas/functions/control-plane/src/scale-runners/cache.ts b/lambdas/functions/control-plane/src/scale-runners/cache.ts index 302a24b2d5..a67d1bdb2a 100644 --- a/lambdas/functions/control-plane/src/scale-runners/cache.ts +++ b/lambdas/functions/control-plane/src/scale-runners/cache.ts @@ -1,4 +1,8 @@ +import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'; import { Octokit } from '@octokit/rest'; +import { createChildLogger } from '@aws-github-runner/aws-powertools-util'; + +const logger = createChildLogger('cache'); export type UnboxPromise = T extends Promise ? U : T; @@ -13,3 +17,230 @@ export class githubCache { githubCache.runners.clear(); } } + +/** + * Cache entry for EC2 runner counts with TTL support. + * This cache helps reduce EC2 DescribeInstances API calls during scale-up operations, + * addressing rate limiting issues in high-volume environments (Issue #4710). + */ +interface EC2RunnerCountCacheEntry { + count: number; + timestamp: number; +} + +/** + * In-memory cache for EC2 runner counts to mitigate EC2 API rate limiting. + * + * This cache stores the count of active runners per environment/type/owner combination + * with a configurable TTL. Within a single Lambda invocation processing batch messages, + * this prevents redundant DescribeInstances calls for the same owner group. + * + * The cache is designed to be slightly stale (short TTL) to reduce API load while + * maintaining accuracy for scaling decisions. In high-throughput environments (20K+ runners/day), + * this can significantly reduce EC2 API throttling issues. + * + * @see https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710 + */ +export class ec2RunnerCountCache { + private static counts: Map = new Map(); + + /** + * Default TTL in milliseconds. 5 seconds provides a good balance between + * reducing API calls and maintaining accuracy for scaling decisions. + */ + private static DEFAULT_TTL_MS = 5000; + + /** + * Resets the cache. Called at the start of each Lambda invocation to ensure + * fresh data for new invocations while still benefiting from caching within + * a single invocation processing multiple messages. + */ + public static reset(): void { + ec2RunnerCountCache.counts.clear(); + } + + /** + * Generates a cache key from the filter parameters. + * Format: "environment#runnerType#runnerOwner" + */ + private static generateKey(environment: string, runnerType: string, runnerOwner: string): string { + return `${environment}#${runnerType}#${runnerOwner}`; + } + + /** + * Gets the cached runner count if available and not expired. + * + * @param environment - The deployment environment (e.g., "prod", "dev") + * @param runnerType - The runner type ("Org" or "Repo") + * @param runnerOwner - The owner (org name or owner/repo) + * @param ttlMs - Optional custom TTL in milliseconds + * @returns The cached count or undefined if not cached or expired + */ + public static get( + environment: string, + runnerType: string, + runnerOwner: string, + ttlMs: number = ec2RunnerCountCache.DEFAULT_TTL_MS, + ): number | undefined { + const key = ec2RunnerCountCache.generateKey(environment, runnerType, runnerOwner); + const cached = ec2RunnerCountCache.counts.get(key); + + if (cached && Date.now() - cached.timestamp < ttlMs) { + return cached.count; + } + + // Entry expired or not found, remove it + if (cached) { + ec2RunnerCountCache.counts.delete(key); + } + + return undefined; + } + + /** + * Sets the runner count in the cache. + * + * @param environment - The deployment environment + * @param runnerType - The runner type + * @param runnerOwner - The owner + * @param count - The current count of runners + */ + public static set(environment: string, runnerType: string, runnerOwner: string, count: number): void { + const key = ec2RunnerCountCache.generateKey(environment, runnerType, runnerOwner); + ec2RunnerCountCache.counts.set(key, { + count, + timestamp: Date.now(), + }); + } + + /** + * Increments the cached count by a specified amount. + * Used after successfully creating new runners to keep the cache accurate + * without requiring a new DescribeInstances call. + * + * @param environment - The deployment environment + * @param runnerType - The runner type + * @param runnerOwner - The owner + * @param increment - The number to add to the current count + */ + public static increment(environment: string, runnerType: string, runnerOwner: string, increment: number): void { + const key = ec2RunnerCountCache.generateKey(environment, runnerType, runnerOwner); + const cached = ec2RunnerCountCache.counts.get(key); + + if (cached) { + cached.count += increment; + cached.timestamp = Date.now(); + } + } + + /** + * Gets the current cache size (for debugging/metrics). + */ + public static size(): number { + return ec2RunnerCountCache.counts.size; + } +} + +/** + * DynamoDB-based persistent cache for EC2 runner counts. + * + * This cache reads from a DynamoDB table that is updated by an EventBridge-triggered + * Lambda function when EC2 instances change state. This provides cross-invocation + * consistency and eliminates EC2 DescribeInstances calls entirely. + * + * The table is expected to have: + * - pk (partition key): "environment#type#owner" format + * - count: atomic counter of active runners + * - updated: timestamp of last update + * + * @see https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710 + */ +export class dynamoDbRunnerCountCache { + private static dynamoClient: DynamoDBClient | null = null; + private static tableName: string | null = null; + private static staleThresholdMs: number = 60000; // 1 minute default + + /** + * Initializes the DynamoDB cache with the required configuration. + * Should be called once at Lambda startup if the cache table is configured. + */ + public static initialize(tableName: string, region: string, staleThresholdMs?: number): void { + dynamoDbRunnerCountCache.tableName = tableName; + dynamoDbRunnerCountCache.dynamoClient = new DynamoDBClient({ region }); + if (staleThresholdMs !== undefined) { + dynamoDbRunnerCountCache.staleThresholdMs = staleThresholdMs; + } + logger.debug('DynamoDB runner count cache initialized', { tableName, staleThresholdMs }); + } + + /** + * Checks if the DynamoDB cache is enabled and initialized. + */ + public static isEnabled(): boolean { + return dynamoDbRunnerCountCache.tableName !== null && dynamoDbRunnerCountCache.dynamoClient !== null; + } + + /** + * Generates a cache key from the filter parameters. + * Format: "environment#runnerType#runnerOwner" + */ + private static generateKey(environment: string, runnerType: string, runnerOwner: string): string { + return `${environment}#${runnerType}#${runnerOwner}`; + } + + /** + * Gets the runner count from DynamoDB if available and not stale. + * + * @param environment - The deployment environment + * @param runnerType - The runner type ("Org" or "Repo") + * @param runnerOwner - The owner (org name or owner/repo) + * @returns Object with count and isStale flag, or null if not found + */ + public static async get( + environment: string, + runnerType: string, + runnerOwner: string, + ): Promise<{ count: number; isStale: boolean } | null> { + if (!dynamoDbRunnerCountCache.isEnabled()) { + return null; + } + + const pk = dynamoDbRunnerCountCache.generateKey(environment, runnerType, runnerOwner); + + try { + const result = await dynamoDbRunnerCountCache.dynamoClient!.send( + new GetItemCommand({ + TableName: dynamoDbRunnerCountCache.tableName!, + Key: { + pk: { S: pk }, + }, + }), + ); + + if (!result.Item) { + logger.debug('No DynamoDB cache entry found', { pk }); + return null; + } + + const count = parseInt(result.Item.count?.N || '0', 10); + const updated = parseInt(result.Item.updated?.N || '0', 10); + const isStale = Date.now() - updated > dynamoDbRunnerCountCache.staleThresholdMs; + + logger.debug('DynamoDB cache hit', { pk, count, isStale, ageMs: Date.now() - updated }); + + return { count: Math.max(0, count), isStale }; + } catch (error) { + logger.warn('Failed to read from DynamoDB cache', { pk, error }); + return null; + } + } + + /** + * Resets the cache configuration (primarily for testing). + */ + public static reset(): void { + dynamoDbRunnerCountCache.dynamoClient = null; + dynamoDbRunnerCountCache.tableName = null; + dynamoDbRunnerCountCache.staleThresholdMs = 60000; + } +} diff --git a/lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts b/lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts index b876d31d50..f96ba69db5 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts @@ -12,6 +12,7 @@ import * as scaleUpModule from './scale-up'; import { getParameter } from '@aws-github-runner/aws-ssm-util'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { Octokit } from '@octokit/rest'; +import { ec2RunnerCountCache, dynamoDbRunnerCountCache } from './cache'; const mockOctokit = { paginate: vi.fn(), @@ -130,6 +131,10 @@ beforeEach(() => { vi.clearAllMocks(); setDefaults(); + // Reset runner count caches to ensure tests start with clean state + ec2RunnerCountCache.reset(); + dynamoDbRunnerCountCache.reset(); + defaultSSMGetParameterMockImpl(); defaultOctokitMockImpl(); 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 35df7ea5d7..a64906f0a2 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-up.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-up.ts @@ -7,9 +7,21 @@ import { createGithubAppAuth, createGithubInstallationAuth, createOctokitClient import { createRunner, listEC2Runners, tag } from './../aws/runners'; import { RunnerInputParameters } from './../aws/runners.d'; import { metricGitHubAppRateLimit } from '../github/rate-limit'; +import { ec2RunnerCountCache, dynamoDbRunnerCountCache } from './cache'; const logger = createChildLogger('scale-up'); +// Initialize DynamoDB cache if configured +const dynamoDbCacheTableName = process.env.RUNNER_COUNT_CACHE_TABLE_NAME; +const dynamoDbCacheStaleThreshold = parseInt(process.env.RUNNER_COUNT_CACHE_STALE_THRESHOLD_MS || '60000', 10); +if (dynamoDbCacheTableName && process.env.AWS_REGION) { + dynamoDbRunnerCountCache.initialize(dynamoDbCacheTableName, process.env.AWS_REGION, dynamoDbCacheStaleThreshold); + logger.info('DynamoDB runner count cache enabled', { + tableName: dynamoDbCacheTableName, + staleThresholdMs: dynamoDbCacheStaleThreshold, + }); +} + export interface RunnerGroup { name: string; id: number; @@ -365,8 +377,44 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise In-memory (within invocation) -> EC2 API + let currentRunners = 0; + if (maximumRunners !== -1) { + let cacheSource = 'none'; + + // First, try DynamoDB cache (cross-invocation, event-driven) + if (dynamoDbRunnerCountCache.isEnabled()) { + const dynamoResult = await dynamoDbRunnerCountCache.get(environment, runnerType, group); + if (dynamoResult !== null && !dynamoResult.isStale) { + currentRunners = dynamoResult.count; + cacheSource = 'dynamodb'; + logger.debug('Using DynamoDB cached runner count', { + currentRunners, + group, + cacheSource, + }); + } + } + + // If DynamoDB cache miss or stale, try in-memory cache + if (cacheSource === 'none') { + const cachedCount = ec2RunnerCountCache.get(environment, runnerType, group); + if (cachedCount !== undefined) { + currentRunners = cachedCount; + cacheSource = 'memory'; + logger.debug('Using in-memory cached runner count', { currentRunners, group, cacheSource }); + } + } + + // If all caches miss, fall back to EC2 API + if (cacheSource === 'none') { + currentRunners = (await listEC2Runners({ environment, runnerType, runnerOwner: group })).length; + ec2RunnerCountCache.set(environment, runnerType, group, currentRunners); + cacheSource = 'ec2-api'; + logger.debug('Fetched runner count from EC2 API', { currentRunners, group, cacheSource }); + } + } logger.info('Current runners', { currentRunners, @@ -436,6 +484,11 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise 0) { + ec2RunnerCountCache.increment(environment, runnerType, group, instances.length); + } + // Not all runners we wanted were created, let's reject enough items so that // number of entries will be retried. if (instances.length !== newRunners) { diff --git a/lambdas/functions/runner-count-cache/package.json b/lambdas/functions/runner-count-cache/package.json new file mode 100644 index 0000000000..ea173203d4 --- /dev/null +++ b/lambdas/functions/runner-count-cache/package.json @@ -0,0 +1,40 @@ +{ + "name": "@aws-github-runner/runner-count-cache", + "version": "1.0.0", + "main": "lambda.ts", + "type": "module", + "license": "MIT", + "scripts": { + "test": "NODE_ENV=test nx test", + "test:watch": "NODE_ENV=test nx test --watch", + "lint": "eslint src", + "build": "ncc build src/lambda.ts -o dist", + "dist": "yarn build && cp package.json dist/ && cd dist && zip ../runner-count-cache.zip *", + "format": "prettier --write \"**/*.ts\"", + "format-check": "prettier --check \"**/*.ts\"", + "all": "yarn build && yarn format && yarn lint && yarn test" + }, + "devDependencies": { + "@aws-sdk/types": "^3.936.0", + "@types/aws-lambda": "^8.10.155", + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "aws-sdk-client-mock": "^4.1.0", + "aws-sdk-client-mock-jest": "^4.1.0" + }, + "dependencies": { + "@aws-github-runner/aws-powertools-util": "*", + "@aws-sdk/client-dynamodb": "^3.948.0", + "@aws-sdk/client-ec2": "^3.948.0" + }, + "nx": { + "includedScripts": [ + "build", + "dist", + "format", + "format-check", + "lint", + "all" + ] + } +} diff --git a/lambdas/functions/runner-count-cache/src/lambda.ts b/lambdas/functions/runner-count-cache/src/lambda.ts new file mode 100644 index 0000000000..64d8c12220 --- /dev/null +++ b/lambdas/functions/runner-count-cache/src/lambda.ts @@ -0,0 +1,176 @@ +/** + * Runner Count Cache Lambda + * + * This Lambda function is triggered by EventBridge when EC2 instances change state. + * It updates an atomic counter in DynamoDB to track the number of active runners + * per environment/type/owner combination. + * + * This eliminates the need for repeated DescribeInstances API calls during scale-up, + * addressing the performance bottleneck described in Issue #4710. + * + * @see https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710 + */ + +import { EventBridgeEvent, Context } from 'aws-lambda'; +import { DynamoDBClient, UpdateItemCommand } from '@aws-sdk/client-dynamodb'; +import { EC2Client, DescribeInstancesCommand } from '@aws-sdk/client-ec2'; +import { createChildLogger, setContext } from '@aws-github-runner/aws-powertools-util'; + +const logger = createChildLogger('runner-count-cache'); + +interface EC2StateChangeDetail { + 'instance-id': string; + state: 'pending' | 'running' | 'shutting-down' | 'stopped' | 'stopping' | 'terminated'; +} + +interface InstanceTags { + environment?: string; + type?: string; + owner?: string; + application?: string; +} + +/** + * Get instance tags from EC2 to determine if this is a managed runner + */ +async function getInstanceTags(ec2: EC2Client, instanceId: string): Promise { + try { + const result = await ec2.send( + new DescribeInstancesCommand({ + InstanceIds: [instanceId], + }), + ); + + const instance = result.Reservations?.[0]?.Instances?.[0]; + if (!instance) { + logger.debug('Instance not found', { instanceId }); + return null; + } + + const tags = instance.Tags || []; + return { + environment: tags.find((t) => t.Key === 'ghr:environment')?.Value, + type: tags.find((t) => t.Key === 'ghr:Type')?.Value, + owner: tags.find((t) => t.Key === 'ghr:Owner')?.Value, + application: tags.find((t) => t.Key === 'ghr:Application')?.Value, + }; + } catch (error) { + // Instance might already be terminated, which is fine + logger.debug('Failed to get instance tags', { instanceId, error }); + return null; + } +} + +/** + * Update the counter in DynamoDB using atomic increment/decrement + */ +async function updateCounter( + dynamodb: DynamoDBClient, + tableName: string, + pk: string, + increment: number, + ttlSeconds: number, +): Promise { + const now = Date.now(); + const ttl = Math.floor(now / 1000) + ttlSeconds; + + await dynamodb.send( + new UpdateItemCommand({ + TableName: tableName, + Key: { + pk: { S: pk }, + }, + UpdateExpression: 'ADD #count :inc SET #updated = :now, #ttl = :ttl', + ExpressionAttributeNames: { + '#count': 'count', + '#updated': 'updated', + '#ttl': 'ttl', + }, + ExpressionAttributeValues: { + ':inc': { N: String(increment) }, + ':now': { N: String(now) }, + ':ttl': { N: String(ttl) }, + }, + }), + ); +} + +/** + * Lambda handler for EC2 state change events + */ +export async function handler( + event: EventBridgeEvent<'EC2 Instance State-change Notification', EC2StateChangeDetail>, + context: Context, +): Promise { + setContext(context, 'lambda.ts'); + + const instanceId = event.detail['instance-id']; + const state = event.detail.state; + const tableName = process.env.DYNAMODB_TABLE_NAME; + const environmentFilter = process.env.ENVIRONMENT_FILTER; + const ttlSeconds = parseInt(process.env.TTL_SECONDS || '86400', 10); + + if (!tableName) { + logger.error('DYNAMODB_TABLE_NAME environment variable not set'); + return; + } + + logger.info('Processing EC2 state change', { instanceId, state }); + + const ec2 = new EC2Client({ region: process.env.AWS_REGION }); + const dynamodb = new DynamoDBClient({ region: process.env.AWS_REGION }); + + // Get instance tags to check if this is a managed runner + const tags = await getInstanceTags(ec2, instanceId); + + if (!tags) { + logger.debug('Could not get instance tags, skipping', { instanceId }); + return; + } + + // Check if this is a GitHub Action runner + if (tags.application !== 'github-action-runner') { + logger.debug('Instance is not a GitHub Action runner, skipping', { instanceId }); + return; + } + + // Check if environment matches our filter + if (environmentFilter && tags.environment !== environmentFilter) { + logger.debug('Instance environment does not match filter, skipping', { + instanceId, + instanceEnv: tags.environment, + filterEnv: environmentFilter, + }); + return; + } + + // Ensure we have required tags + if (!tags.environment || !tags.type || !tags.owner) { + logger.debug('Instance missing required tags, skipping', { instanceId, tags }); + return; + } + + // Generate partition key + const pk = `${tags.environment}#${tags.type}#${tags.owner}`; + + // Determine increment based on state + let increment = 0; + if (state === 'running' || state === 'pending') { + increment = 1; + } else if (state === 'terminated' || state === 'stopped' || state === 'shutting-down') { + increment = -1; + } + + if (increment === 0) { + logger.debug('State does not affect counter', { state }); + return; + } + + try { + await updateCounter(dynamodb, tableName, pk, increment, ttlSeconds); + logger.info('Counter updated', { pk, increment, state }); + } catch (error) { + logger.error('Failed to update counter', { pk, increment, error }); + throw error; + } +} diff --git a/lambdas/functions/runner-count-cache/tsconfig.json b/lambdas/functions/runner-count-cache/tsconfig.json new file mode 100644 index 0000000000..30cbbee83e --- /dev/null +++ b/lambdas/functions/runner-count-cache/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends" : "../../tsconfig.json", + "include": [ + "src/**/*" + ], + "exclude": [ + "src/**/*.test.ts" + ] +} diff --git a/lambdas/functions/runner-count-cache/vitest.config.ts b/lambdas/functions/runner-count-cache/vitest.config.ts new file mode 100644 index 0000000000..2bb87ce333 --- /dev/null +++ b/lambdas/functions/runner-count-cache/vitest.config.ts @@ -0,0 +1,20 @@ +import { resolve } from 'path'; + +import { mergeConfig } from 'vitest/config'; +import defaultConfig from '../../vitest.base.config'; + +export default mergeConfig(defaultConfig, { + test: { + setupFiles: [resolve(__dirname, '../../aws-vitest-setup.ts')], + coverage: { + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/**/*.d.ts'], + thresholds: { + statements: 90, + branches: 90, + functions: 90, + lines: 90, + }, + }, + }, +}); diff --git a/lambdas/yarn.lock b/lambdas/yarn.lock index ffdd9f0e16..7cbc0914e9 100644 --- a/lambdas/yarn.lock +++ b/lambdas/yarn.lock @@ -149,6 +149,7 @@ __metadata: "@aws-github-runner/aws-powertools-util": "npm:*" "@aws-github-runner/aws-ssm-util": "npm:*" "@aws-lambda-powertools/parameters": "npm:^2.29.0" + "@aws-sdk/client-dynamodb": "npm:^3.948.0" "@aws-sdk/client-ec2": "npm:^3.948.0" "@aws-sdk/client-sqs": "npm:^3.948.0" "@aws-sdk/types": "npm:^3.936.0" @@ -192,6 +193,22 @@ __metadata: languageName: unknown linkType: soft +"@aws-github-runner/runner-count-cache@workspace:functions/runner-count-cache": + version: 0.0.0-use.local + resolution: "@aws-github-runner/runner-count-cache@workspace:functions/runner-count-cache" + dependencies: + "@aws-github-runner/aws-powertools-util": "npm:*" + "@aws-sdk/client-dynamodb": "npm:^3.948.0" + "@aws-sdk/client-ec2": "npm:^3.948.0" + "@aws-sdk/types": "npm:^3.936.0" + "@types/aws-lambda": "npm:^8.10.155" + "@types/node": "npm:^22.19.0" + "@vercel/ncc": "npm:^0.38.4" + aws-sdk-client-mock: "npm:^4.1.0" + aws-sdk-client-mock-jest: "npm:^4.1.0" + languageName: unknown + linkType: soft + "@aws-github-runner/termination-watcher@workspace:functions/termination-watcher": version: 0.0.0-use.local resolution: "@aws-github-runner/termination-watcher@workspace:functions/termination-watcher" @@ -318,6 +335,56 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-dynamodb@npm:^3.948.0": + version: 3.962.0 + resolution: "@aws-sdk/client-dynamodb@npm:3.962.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/credential-provider-node": "npm:3.962.0" + "@aws-sdk/dynamodb-codec": "npm:3.957.0" + "@aws-sdk/middleware-endpoint-discovery": "npm:3.957.0" + "@aws-sdk/middleware-host-header": "npm:3.957.0" + "@aws-sdk/middleware-logger": "npm:3.957.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.957.0" + "@aws-sdk/middleware-user-agent": "npm:3.957.0" + "@aws-sdk/region-config-resolver": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@aws-sdk/util-endpoints": "npm:3.957.0" + "@aws-sdk/util-user-agent-browser": "npm:3.957.0" + "@aws-sdk/util-user-agent-node": "npm:3.957.0" + "@smithy/config-resolver": "npm:^4.4.5" + "@smithy/core": "npm:^3.20.0" + "@smithy/fetch-http-handler": "npm:^5.3.8" + "@smithy/hash-node": "npm:^4.2.7" + "@smithy/invalid-dependency": "npm:^4.2.7" + "@smithy/middleware-content-length": "npm:^4.2.7" + "@smithy/middleware-endpoint": "npm:^4.4.1" + "@smithy/middleware-retry": "npm:^4.4.17" + "@smithy/middleware-serde": "npm:^4.2.8" + "@smithy/middleware-stack": "npm:^4.2.7" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/node-http-handler": "npm:^4.4.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-body-length-browser": "npm:^4.2.0" + "@smithy/util-body-length-node": "npm:^4.2.1" + "@smithy/util-defaults-mode-browser": "npm:^4.3.16" + "@smithy/util-defaults-mode-node": "npm:^4.2.19" + "@smithy/util-endpoints": "npm:^3.2.7" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-retry": "npm:^4.2.7" + "@smithy/util-utf8": "npm:^4.2.0" + "@smithy/util-waiter": "npm:^4.2.7" + tslib: "npm:^2.6.2" + checksum: 10c0/d85a0570c4727da3ca64834976bcab371acb747eb1d6cb821fe19fd803ce4e937993dc6f91fec8f0c0dc861d7ea466a2c20fac2467d7966f5ccef944ed9b5b03 + languageName: node + linkType: hard + "@aws-sdk/client-ec2@npm:^3.948.0": version: 3.948.0 resolution: "@aws-sdk/client-ec2@npm:3.948.0" @@ -621,6 +688,52 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/client-sso@npm:3.958.0": + version: 3.958.0 + resolution: "@aws-sdk/client-sso@npm:3.958.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/middleware-host-header": "npm:3.957.0" + "@aws-sdk/middleware-logger": "npm:3.957.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.957.0" + "@aws-sdk/middleware-user-agent": "npm:3.957.0" + "@aws-sdk/region-config-resolver": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@aws-sdk/util-endpoints": "npm:3.957.0" + "@aws-sdk/util-user-agent-browser": "npm:3.957.0" + "@aws-sdk/util-user-agent-node": "npm:3.957.0" + "@smithy/config-resolver": "npm:^4.4.5" + "@smithy/core": "npm:^3.20.0" + "@smithy/fetch-http-handler": "npm:^5.3.8" + "@smithy/hash-node": "npm:^4.2.7" + "@smithy/invalid-dependency": "npm:^4.2.7" + "@smithy/middleware-content-length": "npm:^4.2.7" + "@smithy/middleware-endpoint": "npm:^4.4.1" + "@smithy/middleware-retry": "npm:^4.4.17" + "@smithy/middleware-serde": "npm:^4.2.8" + "@smithy/middleware-stack": "npm:^4.2.7" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/node-http-handler": "npm:^4.4.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-body-length-browser": "npm:^4.2.0" + "@smithy/util-body-length-node": "npm:^4.2.1" + "@smithy/util-defaults-mode-browser": "npm:^4.3.16" + "@smithy/util-defaults-mode-node": "npm:^4.2.19" + "@smithy/util-endpoints": "npm:^3.2.7" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-retry": "npm:^4.2.7" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7d228a59806a8604cee23cd0f2d2fe82b7eeb205b9b535bee2830d8b6242e612d5cfde13443d4d76e6f87a57ac847883b784228a3e5f4d84536602c07d43cab6 + languageName: node + linkType: hard + "@aws-sdk/core@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/core@npm:3.947.0" @@ -642,6 +755,27 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/core@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/core@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@aws-sdk/xml-builder": "npm:3.957.0" + "@smithy/core": "npm:^3.20.0" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/signature-v4": "npm:^5.3.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a98a31b68264efef5a77722e23fdbb20624c2ebe0bdc555da925804d30cb8d76382fa180ef0ae5c4700a453b88fdf8349abb30dea8821d11a7d5dbed1b285c29 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-env@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/credential-provider-env@npm:3.947.0" @@ -655,6 +789,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-env@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.957.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/450dc7f084b510bf62b58ffc2d2e6c390f1b3782801a7f6f2c3c43024f8c391eaec58740813f435d8377a57435c9c73fb7b0430d1eac344e2b3076d2897b8ebe + languageName: node + linkType: hard + "@aws-sdk/credential-provider-http@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/credential-provider-http@npm:3.947.0" @@ -673,6 +820,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-http@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.957.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/fetch-http-handler": "npm:^5.3.8" + "@smithy/node-http-handler": "npm:^4.4.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-stream": "npm:^4.5.8" + tslib: "npm:^2.6.2" + checksum: 10c0/6642859e41241a77592301265444ee86a964a56ee2a9b079d96d879cf959a65279d7d5e7c2ddfd30bcb99f9a0af049190f316a1a398f79ca8bfe3205c7cf10e2 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-ini@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/credential-provider-ini@npm:3.948.0" @@ -695,6 +860,28 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-ini@npm:3.962.0": + version: 3.962.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.962.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/credential-provider-env": "npm:3.957.0" + "@aws-sdk/credential-provider-http": "npm:3.957.0" + "@aws-sdk/credential-provider-login": "npm:3.962.0" + "@aws-sdk/credential-provider-process": "npm:3.957.0" + "@aws-sdk/credential-provider-sso": "npm:3.958.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.958.0" + "@aws-sdk/nested-clients": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/credential-provider-imds": "npm:^4.2.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8754a071f8ea65dbd9ca3490019e91b109dcfd26a48a47a532735c8f7e16771518bca7b322072de0adc5a55cba2b09c30a5bf1727b3b897cb73020f9e371c527 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-login@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/credential-provider-login@npm:3.948.0" @@ -711,6 +898,22 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-login@npm:3.962.0": + version: 3.962.0 + resolution: "@aws-sdk/credential-provider-login@npm:3.962.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/nested-clients": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/43cef761835bcdb0acb7042542f63800b6f84bc6c31558390bc76b72ef9fa5d9fc12c3445bd83b23bbf67bf41fa006ad2ab37e26900766fd8ec22dd94490076a + languageName: node + linkType: hard + "@aws-sdk/credential-provider-node@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/credential-provider-node@npm:3.948.0" @@ -731,6 +934,26 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-node@npm:3.962.0": + version: 3.962.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.962.0" + dependencies: + "@aws-sdk/credential-provider-env": "npm:3.957.0" + "@aws-sdk/credential-provider-http": "npm:3.957.0" + "@aws-sdk/credential-provider-ini": "npm:3.962.0" + "@aws-sdk/credential-provider-process": "npm:3.957.0" + "@aws-sdk/credential-provider-sso": "npm:3.958.0" + "@aws-sdk/credential-provider-web-identity": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/credential-provider-imds": "npm:^4.2.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/47008d09c1b6594a161b343948f1454c5f9ffef9892f2845b03b533462ee9561310f0d8934e4538f87d8853d6b316a0602600aacf949ce9105a99fe6ed4f8840 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-process@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/credential-provider-process@npm:3.947.0" @@ -745,6 +968,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-process@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.957.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/103f274ebc1aa89bc183bde13d2979af623ff9a1c502cda09304e0b8ec421c7bb45dd1a6825fbb57ef11f16cc58f481d20d527eb97205a1f2a3c5f610814ffee + languageName: node + linkType: hard + "@aws-sdk/credential-provider-sso@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/credential-provider-sso@npm:3.948.0" @@ -761,6 +998,22 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-sso@npm:3.958.0": + version: 3.958.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.958.0" + dependencies: + "@aws-sdk/client-sso": "npm:3.958.0" + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/token-providers": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/0fcaf59c3c473cd6f4055db4d6e6a3c2a14e7630177aa9b44eced26d1d6e9e6c48f31983260466e488f57517c6d119635fdd30707158967c234a751a06a0be28 + languageName: node + linkType: hard + "@aws-sdk/credential-provider-web-identity@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/credential-provider-web-identity@npm:3.948.0" @@ -776,6 +1029,47 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/credential-provider-web-identity@npm:3.958.0": + version: 3.958.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.958.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/nested-clients": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/a4aa165c7ca2a253d0a6728b2b91421dd94945dc272562b7bb0347fa8fd334c54249d62b9d84846b0051aef60ea6a86c8d20a2e63da656624acaad8cb10cb5bf + languageName: node + linkType: hard + +"@aws-sdk/dynamodb-codec@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/dynamodb-codec@npm:3.957.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@smithy/core": "npm:^3.20.0" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-base64": "npm:^4.3.0" + tslib: "npm:^2.6.2" + peerDependencies: + "@aws-sdk/client-dynamodb": ^3.957.0 + checksum: 10c0/babfeaffce4f2cab83467d3d71f9d0f1b063d85505f02d8fe60dcef6046e3253e7380eb365b3fb7547649c015c95a0e104528bb0dc42da6df67d17cf81b5285f + languageName: node + linkType: hard + +"@aws-sdk/endpoint-cache@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/endpoint-cache@npm:3.957.0" + dependencies: + mnemonist: "npm:0.38.3" + tslib: "npm:^2.6.2" + checksum: 10c0/af356c17a3c0bab452c53d2b1c9cd12c26e066581cd85ef8e49d3ac0586b717d01dfa073899474a5a7a17722fbf457067337beb1ee483354864cacf0bd56a485 + languageName: node + linkType: hard + "@aws-sdk/lib-storage@npm:^3.948.0": version: 3.948.0 resolution: "@aws-sdk/lib-storage@npm:3.948.0" @@ -808,6 +1102,20 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-endpoint-discovery@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/middleware-endpoint-discovery@npm:3.957.0" + dependencies: + "@aws-sdk/endpoint-cache": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/acdc8d2e18779057c81b0c97c404c53c189183d7373a789ac7f1c70f4dc2625f7eb065575eb20f3a1a2da5af588069d67ac46027a4f1e48b16b44cddbe75ef15 + languageName: node + linkType: hard + "@aws-sdk/middleware-expect-continue@npm:3.936.0": version: 3.936.0 resolution: "@aws-sdk/middleware-expect-continue@npm:3.936.0" @@ -853,6 +1161,18 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-host-header@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/b7656460ff07a14703b45a0047a6cb81c53e0cfc0b7ebe1e8079edfb09a4400ff09e537b6a90871d005c92066c4a450ce8987fa280680286063d32e13c901883 + languageName: node + linkType: hard + "@aws-sdk/middleware-location-constraint@npm:3.936.0": version: 3.936.0 resolution: "@aws-sdk/middleware-location-constraint@npm:3.936.0" @@ -875,6 +1195,17 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-logger@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/middleware-logger@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/767707c6748c3e4035339119d3c3a12d79402d99acc37dd96a83099a1201e9130a669d5fb5f4f15d8f67e776a6a3e5d99365b19a67b4eb30aff02d33f6cbc35b + languageName: node + linkType: hard + "@aws-sdk/middleware-recursion-detection@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/middleware-recursion-detection@npm:3.948.0" @@ -888,6 +1219,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-recursion-detection@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@aws/lambda-invoke-store": "npm:^0.2.2" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/21808cad790cc052f420b716b04980d1d974a17fe495ebe7d95e617d566328410f856798eef135ce85d14ea4caf1fa21f34041b41977b5c5d6d0b2b694f7bb78 + languageName: node + linkType: hard + "@aws-sdk/middleware-sdk-ec2@npm:3.946.0": version: 3.946.0 resolution: "@aws-sdk/middleware-sdk-ec2@npm:3.946.0" @@ -966,6 +1310,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/middleware-user-agent@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.957.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@aws-sdk/util-endpoints": "npm:3.957.0" + "@smithy/core": "npm:^3.20.0" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/94287fb18a5dff73e27d16bfd0599df12608ef3cf215978e44a7c389b97089e807bea3bad7a754a56dd5346b84e30ecd1711a6b33621d925883bda0e61ce9da1 + languageName: node + linkType: hard + "@aws-sdk/nested-clients@npm:3.948.0": version: 3.948.0 resolution: "@aws-sdk/nested-clients@npm:3.948.0" @@ -1012,6 +1371,52 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/nested-clients@npm:3.958.0": + version: 3.958.0 + resolution: "@aws-sdk/nested-clients@npm:3.958.0" + dependencies: + "@aws-crypto/sha256-browser": "npm:5.2.0" + "@aws-crypto/sha256-js": "npm:5.2.0" + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/middleware-host-header": "npm:3.957.0" + "@aws-sdk/middleware-logger": "npm:3.957.0" + "@aws-sdk/middleware-recursion-detection": "npm:3.957.0" + "@aws-sdk/middleware-user-agent": "npm:3.957.0" + "@aws-sdk/region-config-resolver": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@aws-sdk/util-endpoints": "npm:3.957.0" + "@aws-sdk/util-user-agent-browser": "npm:3.957.0" + "@aws-sdk/util-user-agent-node": "npm:3.957.0" + "@smithy/config-resolver": "npm:^4.4.5" + "@smithy/core": "npm:^3.20.0" + "@smithy/fetch-http-handler": "npm:^5.3.8" + "@smithy/hash-node": "npm:^4.2.7" + "@smithy/invalid-dependency": "npm:^4.2.7" + "@smithy/middleware-content-length": "npm:^4.2.7" + "@smithy/middleware-endpoint": "npm:^4.4.1" + "@smithy/middleware-retry": "npm:^4.4.17" + "@smithy/middleware-serde": "npm:^4.2.8" + "@smithy/middleware-stack": "npm:^4.2.7" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/node-http-handler": "npm:^4.4.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-body-length-browser": "npm:^4.2.0" + "@smithy/util-body-length-node": "npm:^4.2.1" + "@smithy/util-defaults-mode-browser": "npm:^4.3.16" + "@smithy/util-defaults-mode-node": "npm:^4.2.19" + "@smithy/util-endpoints": "npm:^3.2.7" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-retry": "npm:^4.2.7" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/1dd741dc13a7ffb763960aac4066f91d3db332f5bab55a2acbe79707c3373f29cd08ce1358578b70a19245c841c01999ca1f3c204b5daecfaa41bf7353f216b3 + languageName: node + linkType: hard + "@aws-sdk/region-config-resolver@npm:3.936.0": version: 3.936.0 resolution: "@aws-sdk/region-config-resolver@npm:3.936.0" @@ -1025,6 +1430,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/region-config-resolver@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@smithy/config-resolver": "npm:^4.4.5" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/09b6830688e716938d4f9f5afa9e818f08a68213dcbc31c06f1a876d68bd83c2816cb6c148f3491030c84d4ea9dfca79a3cf49b14208d7c6c2d40b3bb11cd42f + languageName: node + linkType: hard + "@aws-sdk/signature-v4-multi-region@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/signature-v4-multi-region@npm:3.947.0" @@ -1054,6 +1472,21 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/token-providers@npm:3.958.0": + version: 3.958.0 + resolution: "@aws-sdk/token-providers@npm:3.958.0" + dependencies: + "@aws-sdk/core": "npm:3.957.0" + "@aws-sdk/nested-clients": "npm:3.958.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8bb1ddaa6df1ec1fe1498ac80e28604ab1da85e30107f80619dc78378b019cba3484b860934f823a17315f726fc7fc00cbb37a649259d0daca807f5c629e773a + languageName: node + linkType: hard + "@aws-sdk/types@npm:3.936.0, @aws-sdk/types@npm:^3.936.0": version: 3.936.0 resolution: "@aws-sdk/types@npm:3.936.0" @@ -1064,6 +1497,16 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/types@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/types@npm:3.957.0" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8dd9826eff9806689a3321f58ffa51c7731599f0d30b22fb744e82bf0914a1e714b622d319ee00315d4a3d814b895efaff87ea1edf23f0f50c26d2b4de31fe7b + languageName: node + linkType: hard + "@aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.4.1": version: 3.914.0 resolution: "@aws-sdk/types@npm:3.914.0" @@ -1096,6 +1539,19 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-endpoints@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/util-endpoints@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + "@smithy/util-endpoints": "npm:^3.2.7" + tslib: "npm:^2.6.2" + checksum: 10c0/21baf87036d3c6c6ace5e3960af6ffdb6e7c0bf7253904c5faae7fad4cc3d7709bef7c3a19a6347d7511ecf50313e0c7754d470d7ed45dabbcaab3232f5b1c87 + languageName: node + linkType: hard + "@aws-sdk/util-format-url@npm:3.936.0": version: 3.936.0 resolution: "@aws-sdk/util-format-url@npm:3.936.0" @@ -1129,6 +1585,18 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-browser@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.957.0" + dependencies: + "@aws-sdk/types": "npm:3.957.0" + "@smithy/types": "npm:^4.11.0" + bowser: "npm:^2.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/342da4e37bcb193cd5564fd3f9ae5875405ad87b411703507e7ddfe9875f24f889afd80d4167c2de57218057a45d1c6c1dc4780d3966537da1e4e6385bd386ef + languageName: node + linkType: hard + "@aws-sdk/util-user-agent-node@npm:3.947.0": version: 3.947.0 resolution: "@aws-sdk/util-user-agent-node@npm:3.947.0" @@ -1147,6 +1615,24 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/util-user-agent-node@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.957.0" + dependencies: + "@aws-sdk/middleware-user-agent": "npm:3.957.0" + "@aws-sdk/types": "npm:3.957.0" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 10c0/2291d45f056526ed59ed5cefe66557d3f25d11ef080d2fd4d9b313e4e79b4021332b1dfaf9fdce3f8c00d624a0e5dda110caa7d81ff0d46fca87d44faff53757 + languageName: node + linkType: hard + "@aws-sdk/xml-builder@npm:3.930.0": version: 3.930.0 resolution: "@aws-sdk/xml-builder@npm:3.930.0" @@ -1158,6 +1644,17 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/xml-builder@npm:3.957.0": + version: 3.957.0 + resolution: "@aws-sdk/xml-builder@npm:3.957.0" + dependencies: + "@smithy/types": "npm:^4.11.0" + fast-xml-parser: "npm:5.2.5" + tslib: "npm:^2.6.2" + checksum: 10c0/32eb0fff54a790640b3243802ab9bfdfe2d1734ba23745daaa71f3ab655c1ed19058da803bdfa145250379fd6867e9fc02ec7f5bacff3bf2b3de6c7a3703d491 + languageName: node + linkType: hard + "@aws/lambda-invoke-store@npm:0.2.1": version: 0.2.1 resolution: "@aws/lambda-invoke-store@npm:0.2.1" @@ -4180,6 +4677,16 @@ __metadata: languageName: node linkType: hard +"@smithy/abort-controller@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/abort-controller@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/4f992bdff9f035a62c1403da1999e0170f8703a4ad0c7fbc93bc992d4ffcb20d12cebf40ad6dc006c7f0a7e80253646a147ee64ca29266dd7e52800f0ebf93fe + languageName: node + linkType: hard + "@smithy/chunked-blob-reader-native@npm:^4.2.1": version: 4.2.1 resolution: "@smithy/chunked-blob-reader-native@npm:4.2.1" @@ -4213,6 +4720,20 @@ __metadata: languageName: node linkType: hard +"@smithy/config-resolver@npm:^4.4.5": + version: 4.4.5 + resolution: "@smithy/config-resolver@npm:4.4.5" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-config-provider": "npm:^4.2.0" + "@smithy/util-endpoints": "npm:^3.2.7" + "@smithy/util-middleware": "npm:^4.2.7" + tslib: "npm:^2.6.2" + checksum: 10c0/0a7c365bc50e82c9e22897b26cafe1d2a176b425a1303ff55fd5bd5f851e85534e7147d2a1408328dc6ca29f535143eab3289a39d03969e924302226711c0d55 + languageName: node + linkType: hard + "@smithy/core@npm:^3.18.7": version: 3.18.7 resolution: "@smithy/core@npm:3.18.7" @@ -4231,6 +4752,24 @@ __metadata: languageName: node linkType: hard +"@smithy/core@npm:^3.20.0": + version: 3.20.0 + resolution: "@smithy/core@npm:3.20.0" + dependencies: + "@smithy/middleware-serde": "npm:^4.2.8" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-body-length-browser": "npm:^4.2.0" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-stream": "npm:^4.5.8" + "@smithy/util-utf8": "npm:^4.2.0" + "@smithy/uuid": "npm:^1.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/70ef9659b831573a27f68f689658090540da281459c29f61174e3162f2eae10c1dd2c5d959149b8ca00419d199e1de2aebc2b2810a0d009336ebf3a88f7df5b3 + languageName: node + linkType: hard + "@smithy/credential-provider-imds@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/credential-provider-imds@npm:4.2.5" @@ -4244,6 +4783,19 @@ __metadata: languageName: node linkType: hard +"@smithy/credential-provider-imds@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/credential-provider-imds@npm:4.2.7" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + tslib: "npm:^2.6.2" + checksum: 10c0/5c190b46879a9ce12c73099db4fd302089de79b5efd4177be256faa096778817cb9bc8e682f01abe397482ed90b00a726888630aecaaed47c2e3214169a23351 + languageName: node + linkType: hard + "@smithy/eventstream-codec@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/eventstream-codec@npm:4.2.5" @@ -4312,6 +4864,19 @@ __metadata: languageName: node linkType: hard +"@smithy/fetch-http-handler@npm:^5.3.8": + version: 5.3.8 + resolution: "@smithy/fetch-http-handler@npm:5.3.8" + dependencies: + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/querystring-builder": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-base64": "npm:^4.3.0" + tslib: "npm:^2.6.2" + checksum: 10c0/94ca27084fe0aa1626f5dec3755811d61bb7ec81c0a3d9428c324b238495e695f568800e30fdb127129fba95625355d8c51cbcae52796a008c7cfd4ff5074cb5 + languageName: node + linkType: hard + "@smithy/hash-blob-browser@npm:^4.2.6": version: 4.2.6 resolution: "@smithy/hash-blob-browser@npm:4.2.6" @@ -4336,6 +4901,18 @@ __metadata: languageName: node linkType: hard +"@smithy/hash-node@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/hash-node@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + "@smithy/util-buffer-from": "npm:^4.2.0" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/fa3b2194c22dd240b8dcfc191ca68ed563513fc7e852537eb69223933e70a50b365fb53d1a150a37a091cf6d449b4b7aecaa51892b9f49fd3763174e27e1ec5c + languageName: node + linkType: hard + "@smithy/hash-stream-node@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/hash-stream-node@npm:4.2.5" @@ -4357,6 +4934,16 @@ __metadata: languageName: node linkType: hard +"@smithy/invalid-dependency@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/invalid-dependency@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/eadbdd4e7dd94f7caa8c17c003e4c48ef03ff2af0401fab3884468535b016cf318c95e57cdad2b170cb852119303e5500f3bb138635705e8a4d6a2fc58a111ed + languageName: node + linkType: hard + "@smithy/is-array-buffer@npm:^2.2.0": version: 2.2.0 resolution: "@smithy/is-array-buffer@npm:2.2.0" @@ -4397,6 +4984,17 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-content-length@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/middleware-content-length@npm:4.2.7" + dependencies: + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/23237a15d0a39b95157c3d370edd48aeb0be23daff78b858c3a2e8af081c1a91ef6b5800d2746d9c8094e7af7d4aeb44bb2b400b887527bcdab3be4dc0c3b46c + languageName: node + linkType: hard + "@smithy/middleware-endpoint@npm:^4.3.14": version: 4.3.14 resolution: "@smithy/middleware-endpoint@npm:4.3.14" @@ -4413,6 +5011,22 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-endpoint@npm:^4.4.1": + version: 4.4.1 + resolution: "@smithy/middleware-endpoint@npm:4.4.1" + dependencies: + "@smithy/core": "npm:^3.20.0" + "@smithy/middleware-serde": "npm:^4.2.8" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/url-parser": "npm:^4.2.7" + "@smithy/util-middleware": "npm:^4.2.7" + tslib: "npm:^2.6.2" + checksum: 10c0/75fb74725ce5c4c2f689fcf7bf3d3e367d1db95440acc236fa0dce021794c40979170be4e254fa54c717d10feffaaca18eb0d40b5e0d9736ca184af87153bc36 + languageName: node + linkType: hard + "@smithy/middleware-retry@npm:^4.4.14": version: 4.4.14 resolution: "@smithy/middleware-retry@npm:4.4.14" @@ -4430,6 +5044,23 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-retry@npm:^4.4.17": + version: 4.4.17 + resolution: "@smithy/middleware-retry@npm:4.4.17" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/service-error-classification": "npm:^4.2.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-retry": "npm:^4.2.7" + "@smithy/uuid": "npm:^1.1.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8959721163dc1d132889c24744880d33cdf5323c76792d09c026dde338fe4df841e98fa6cf0a27fcbc94982b30431c7dd3020f69595e101433669ed5a610f928 + languageName: node + linkType: hard + "@smithy/middleware-serde@npm:^4.2.6": version: 4.2.6 resolution: "@smithy/middleware-serde@npm:4.2.6" @@ -4441,6 +5072,17 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-serde@npm:^4.2.8": + version: 4.2.8 + resolution: "@smithy/middleware-serde@npm:4.2.8" + dependencies: + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/5ed53af095d605940b540253c38a723d2cc37400c116071455a23b17fdf60af59b74b67b15d84f7bfb3738c9c37e3664b57f24c670d5c96ee46737225c147344 + languageName: node + linkType: hard + "@smithy/middleware-stack@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/middleware-stack@npm:4.2.5" @@ -4451,6 +5093,16 @@ __metadata: languageName: node linkType: hard +"@smithy/middleware-stack@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/middleware-stack@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/199aa2575a8e4e3fa1a1a7989958e2f3aeb8dae115b41547d8bef18b5573e369d7eacc206ec6648194cdce491fe8c54abcccedd4a5c0bca370a11c480bd11ca7 + languageName: node + linkType: hard + "@smithy/node-config-provider@npm:^4.3.5": version: 4.3.5 resolution: "@smithy/node-config-provider@npm:4.3.5" @@ -4463,6 +5115,18 @@ __metadata: languageName: node linkType: hard +"@smithy/node-config-provider@npm:^4.3.7": + version: 4.3.7 + resolution: "@smithy/node-config-provider@npm:4.3.7" + dependencies: + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/shared-ini-file-loader": "npm:^4.4.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/2fbe9f22e315253d8d4f5a8d16d41d36ff4467c9f7e515d456c3172b59af8fbd67004d0d44bdb7638886eb6057d04ce269f84de65a382d2ccd0c08114dea840c + languageName: node + linkType: hard + "@smithy/node-http-handler@npm:^4.4.5": version: 4.4.5 resolution: "@smithy/node-http-handler@npm:4.4.5" @@ -4476,6 +5140,19 @@ __metadata: languageName: node linkType: hard +"@smithy/node-http-handler@npm:^4.4.7": + version: 4.4.7 + resolution: "@smithy/node-http-handler@npm:4.4.7" + dependencies: + "@smithy/abort-controller": "npm:^4.2.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/querystring-builder": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/8f1114b2bc2232b50c0777b58ab5195c91a5aa1a76c48de7aa403f0c3245be287b070498924845036ab558b28827df916c9730f975a1edfc2e7345d1022350c1 + languageName: node + linkType: hard + "@smithy/property-provider@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/property-provider@npm:4.2.5" @@ -4486,6 +5163,16 @@ __metadata: languageName: node linkType: hard +"@smithy/property-provider@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/property-provider@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/7caaeec11262a169c6509c5cd687900342ab02900f3074e54aeafbd2ce8a112c83ce3190225b2dab9f2a7f737f7176960329f882935ae7bd9d624984387d0fc1 + languageName: node + linkType: hard + "@smithy/protocol-http@npm:^5.3.5": version: 5.3.5 resolution: "@smithy/protocol-http@npm:5.3.5" @@ -4496,6 +5183,16 @@ __metadata: languageName: node linkType: hard +"@smithy/protocol-http@npm:^5.3.7": + version: 5.3.7 + resolution: "@smithy/protocol-http@npm:5.3.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/c5bde38fbb71a63e2e25e33792d8b186523afbe1d520ffc821943c40eb41ca804a99afca7917798337b1f2bdea4ae64d3ae745f1036f7e65291d7c7ff301a953 + languageName: node + linkType: hard + "@smithy/querystring-builder@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/querystring-builder@npm:4.2.5" @@ -4507,6 +5204,17 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-builder@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/querystring-builder@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + "@smithy/util-uri-escape": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/24e3b2a35d2828fb19b4213b823b5adc0ce7edcf8e096a618e2dfcd9df3c2a750ee518af4b754759ab49b2a656c5cb66989d6fbbcfc085f8511dc9e02a0e2dce + languageName: node + linkType: hard + "@smithy/querystring-parser@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/querystring-parser@npm:4.2.5" @@ -4517,6 +5225,16 @@ __metadata: languageName: node linkType: hard +"@smithy/querystring-parser@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/querystring-parser@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/4efddf97b35e7b2a04018acf5afd0f658506242adab77098b32e4bb625c5a607fdcfd9df2a7504dcacc7ac5e8624757abb881b2013862a098319a08b5c75a0d1 + languageName: node + linkType: hard + "@smithy/service-error-classification@npm:^2.0.4": version: 2.1.5 resolution: "@smithy/service-error-classification@npm:2.1.5" @@ -4535,6 +5253,15 @@ __metadata: languageName: node linkType: hard +"@smithy/service-error-classification@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/service-error-classification@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + checksum: 10c0/ddbbae91b4eb83ee66262059a3ce0fa2cee7874bcc0704481f5681966ef25af175afe8bfef7cd0868d86901d08cfb61fe34964f5a4c8f7a6347228a34e40845b + languageName: node + linkType: hard + "@smithy/shared-ini-file-loader@npm:^4.4.0": version: 4.4.0 resolution: "@smithy/shared-ini-file-loader@npm:4.4.0" @@ -4545,6 +5272,16 @@ __metadata: languageName: node linkType: hard +"@smithy/shared-ini-file-loader@npm:^4.4.2": + version: 4.4.2 + resolution: "@smithy/shared-ini-file-loader@npm:4.4.2" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/3d401b87b21113aa9bb7490d80ec02d7655c1abc1b23eb384fea13b7e1348f1c599011ed109a3fe2e3675b3bc51f91f43b66d7e46f565f78c3f0d45d3b997058 + languageName: node + linkType: hard + "@smithy/signature-v4@npm:^5.3.5": version: 5.3.5 resolution: "@smithy/signature-v4@npm:5.3.5" @@ -4561,6 +5298,37 @@ __metadata: languageName: node linkType: hard +"@smithy/signature-v4@npm:^5.3.7": + version: 5.3.7 + resolution: "@smithy/signature-v4@npm:5.3.7" + dependencies: + "@smithy/is-array-buffer": "npm:^4.2.0" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-hex-encoding": "npm:^4.2.0" + "@smithy/util-middleware": "npm:^4.2.7" + "@smithy/util-uri-escape": "npm:^4.2.0" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/01cae99baa7298adadbce6b293548adf1520fa8072086b48a0ef64cb13c3a156cb4d575753fc72af8fe0b50c65fa364ccce8931bd0d8ffe398d210da96efb54a + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^4.10.2": + version: 4.10.2 + resolution: "@smithy/smithy-client@npm:4.10.2" + dependencies: + "@smithy/core": "npm:^3.20.0" + "@smithy/middleware-endpoint": "npm:^4.4.1" + "@smithy/middleware-stack": "npm:^4.2.7" + "@smithy/protocol-http": "npm:^5.3.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-stream": "npm:^4.5.8" + tslib: "npm:^2.6.2" + checksum: 10c0/d272f7eab4f7569b2146227bb869c8e8cd7f25ca71da6416b2c6deb3c10717c5699e132224ae8d1c46dfaf7dab4368cb1515bf58dddb288c3150fb86a2faa7b8 + languageName: node + linkType: hard + "@smithy/smithy-client@npm:^4.9.10": version: 4.9.10 resolution: "@smithy/smithy-client@npm:4.9.10" @@ -4585,6 +5353,15 @@ __metadata: languageName: node linkType: hard +"@smithy/types@npm:^4.11.0": + version: 4.11.0 + resolution: "@smithy/types@npm:4.11.0" + dependencies: + tslib: "npm:^2.6.2" + checksum: 10c0/8be4af86df4a78fe43afe7dc3f875bf8ec6ce7c04f7bb167152bf3c7ab2eef26db38ed7ae365c2f283e8796e40372b01b4c857b8db43da393002c5638ef3f249 + languageName: node + linkType: hard + "@smithy/types@npm:^4.8.0": version: 4.8.0 resolution: "@smithy/types@npm:4.8.0" @@ -4614,6 +5391,17 @@ __metadata: languageName: node linkType: hard +"@smithy/url-parser@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/url-parser@npm:4.2.7" + dependencies: + "@smithy/querystring-parser": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ca78587b15a843cc62f3439ae062a24f217e90fa0ec3e50a1ada09cf75c681afa1ccb92ca7a90f63c8f53627d51c6e0c83140422ce98713e1f4866c725923ec0 + languageName: node + linkType: hard + "@smithy/util-base64@npm:^4.3.0": version: 4.3.0 resolution: "@smithy/util-base64@npm:4.3.0" @@ -4684,6 +5472,18 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-browser@npm:^4.3.16": + version: 4.3.16 + resolution: "@smithy/util-defaults-mode-browser@npm:4.3.16" + dependencies: + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/f2bfab553b77ec10d0b4fb659ad63ba48b2a46a52e2df796486c74d7c5b9a5bc5704e44a795a0c39dac997e4243b9ff399f0d33206741c21c9f74f0a15903fad + languageName: node + linkType: hard + "@smithy/util-defaults-mode-node@npm:^4.2.16": version: 4.2.16 resolution: "@smithy/util-defaults-mode-node@npm:4.2.16" @@ -4699,6 +5499,21 @@ __metadata: languageName: node linkType: hard +"@smithy/util-defaults-mode-node@npm:^4.2.19": + version: 4.2.19 + resolution: "@smithy/util-defaults-mode-node@npm:4.2.19" + dependencies: + "@smithy/config-resolver": "npm:^4.4.5" + "@smithy/credential-provider-imds": "npm:^4.2.7" + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/property-provider": "npm:^4.2.7" + "@smithy/smithy-client": "npm:^4.10.2" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/0f68a66ed2cf27f2f8ad8ff796cd4c2f8a6edddd788d5d1ffbbdc2da462f9b7f9d1b49b5c698a204ddee86d4ee238ce10280ddfd5ce748a87b03ebcf2e92bf92 + languageName: node + linkType: hard + "@smithy/util-endpoints@npm:^3.2.5": version: 3.2.5 resolution: "@smithy/util-endpoints@npm:3.2.5" @@ -4710,6 +5525,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-endpoints@npm:^3.2.7": + version: 3.2.7 + resolution: "@smithy/util-endpoints@npm:3.2.7" + dependencies: + "@smithy/node-config-provider": "npm:^4.3.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/ca4b134e0ed8b62dfedb82b9ea91fa028567732e271e934b9b878a9aa43f1c5c9d8860ad49f60992290c7705b7b6d2e734769304b9ea38eec40eaf524bb27ad8 + languageName: node + linkType: hard + "@smithy/util-hex-encoding@npm:^4.2.0": version: 4.2.0 resolution: "@smithy/util-hex-encoding@npm:4.2.0" @@ -4729,6 +5555,16 @@ __metadata: languageName: node linkType: hard +"@smithy/util-middleware@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/util-middleware@npm:4.2.7" + dependencies: + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/76c598cfe8062b6daf0bf88bc855544ce071f1d2df5d9d2c2d1c08402a577cb9ade8f33102a869dfb8aae9f679b86b5faacc9011b032bf453ced255fd8d0a0d3 + languageName: node + linkType: hard + "@smithy/util-retry@npm:^4.2.5": version: 4.2.5 resolution: "@smithy/util-retry@npm:4.2.5" @@ -4740,6 +5576,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-retry@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/util-retry@npm:4.2.7" + dependencies: + "@smithy/service-error-classification": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/51445769ce5382a85f5c78758d6d7d631b3a3f8277fa49ae2c2730536b1a53babfe27efb30e4b96ebc68faead2aafa9ab877e6ed728eb8018d080e26d9a42f58 + languageName: node + linkType: hard + "@smithy/util-stream@npm:^4.5.6": version: 4.5.6 resolution: "@smithy/util-stream@npm:4.5.6" @@ -4756,6 +5603,22 @@ __metadata: languageName: node linkType: hard +"@smithy/util-stream@npm:^4.5.8": + version: 4.5.8 + resolution: "@smithy/util-stream@npm:4.5.8" + dependencies: + "@smithy/fetch-http-handler": "npm:^5.3.8" + "@smithy/node-http-handler": "npm:^4.4.7" + "@smithy/types": "npm:^4.11.0" + "@smithy/util-base64": "npm:^4.3.0" + "@smithy/util-buffer-from": "npm:^4.2.0" + "@smithy/util-hex-encoding": "npm:^4.2.0" + "@smithy/util-utf8": "npm:^4.2.0" + tslib: "npm:^2.6.2" + checksum: 10c0/71f43fdf93ccde982edf4ae3b481006dd42146d17f6594abcca21e2f41e5b40ad69d6038052e016f7135011586294d6ed8c778465ea076deaa50b7808f66bc32 + languageName: node + linkType: hard + "@smithy/util-uri-escape@npm:^4.2.0": version: 4.2.0 resolution: "@smithy/util-uri-escape@npm:4.2.0" @@ -4796,6 +5659,17 @@ __metadata: languageName: node linkType: hard +"@smithy/util-waiter@npm:^4.2.7": + version: 4.2.7 + resolution: "@smithy/util-waiter@npm:4.2.7" + dependencies: + "@smithy/abort-controller": "npm:^4.2.7" + "@smithy/types": "npm:^4.11.0" + tslib: "npm:^2.6.2" + checksum: 10c0/0de99074db038eb09c4ebe2ed7f0ff3a13aa0ce5baf0b62a4b684f282e772e281f9eab8936d7aa577d8f419b676df60aa752e3c2b5edf07b44d8e999d983253f + languageName: node + linkType: hard + "@smithy/uuid@npm:^1.1.0": version: 1.1.0 resolution: "@smithy/uuid@npm:1.1.0" @@ -8747,6 +9621,15 @@ __metadata: languageName: node linkType: hard +"mnemonist@npm:0.38.3": + version: 0.38.3 + resolution: "mnemonist@npm:0.38.3" + dependencies: + obliterator: "npm:^1.6.1" + checksum: 10c0/064aa1ee1a89fce2754423b3617c598fd65bc34311eb3c01dc063976f6b819b073bd23532415cf8c92240157b4c8fbb7ec5d79d717f2bd4fcd95d8131cb23acb + languageName: node + linkType: hard + "moment-timezone@npm:^0.6.0": version: 0.6.0 resolution: "moment-timezone@npm:0.6.0" @@ -9147,6 +10030,13 @@ __metadata: languageName: node linkType: hard +"obliterator@npm:^1.6.1": + version: 1.6.1 + resolution: "obliterator@npm:1.6.1" + checksum: 10c0/5fad57319aae0ef6e34efa640541d41c2dd9790a7ab808f17dcb66c83a81333963fc2dfcfa6e1b62158e5cef6291cdcf15c503ad6c3de54b2227dd4c3d7e1b55 + languageName: node + linkType: hard + "obug@npm:^2.1.1": version: 2.1.1 resolution: "obug@npm:2.1.1" diff --git a/main.tf b/main.tf index a8c501bc9a..4de277a1eb 100644 --- a/main.tf +++ b/main.tf @@ -275,6 +275,11 @@ module "runners" { metrics = var.metrics job_retry = var.job_retry + + runner_count_cache = var.runner_count_cache.enable ? { + table_name = module.runner_count_cache[0].dynamodb_table.name + stale_threshold_ms = var.runner_count_cache.stale_threshold_ms + } : null } module "runner_binaries" { @@ -383,3 +388,32 @@ module "instance_termination_watcher" { config = merge(local.lambda_instance_termination_watcher, var.instance_termination_watcher) } + +module "runner_count_cache" { + source = "./modules/runner-count-cache" + count = var.runner_count_cache.enable ? 1 : 0 + + prefix = var.prefix + tags = local.tags + kms_key_arn = var.kms_key_arn + + environment_filter = var.prefix + ttl_seconds = var.runner_count_cache.ttl_seconds + + lambda_s3_bucket = var.lambda_s3_bucket + counter_lambda_s3_key = var.runner_count_cache.lambda_s3_key + counter_lambda_s3_object_version = var.runner_count_cache.lambda_s3_object_version + counter_lambda_memory_size = var.runner_count_cache.lambda_memory_size + counter_lambda_timeout = var.runner_count_cache.lambda_timeout + lambda_runtime = var.lambda_runtime + lambda_architecture = var.lambda_architecture + lambda_tags = var.lambda_tags + lambda_subnet_ids = var.lambda_subnet_ids + lambda_security_group_ids = var.lambda_security_group_ids + tracing_config = var.tracing_config + logging_retention_in_days = var.logging_retention_in_days + logging_kms_key_id = var.logging_kms_key_id + + role_path = var.role_path + role_permissions_boundary = var.role_permissions_boundary +} diff --git a/modules/runner-count-cache/README.md b/modules/runner-count-cache/README.md new file mode 100644 index 0000000000..28a21c0905 --- /dev/null +++ b/modules/runner-count-cache/README.md @@ -0,0 +1,98 @@ +# Runner Count Cache Module + +This module provides a DynamoDB-based caching system for tracking the number of active EC2 runners. It significantly reduces the need for EC2 `DescribeInstances` API calls during scale-up operations, addressing performance bottlenecks in high-volume environments. + +## Problem Statement + +In large-scale deployments (20K+ runners per day), the scale-up Lambda function's use of `DescribeInstances` to count current runners can: + +- Cause EC2 API rate limiting (throttling) +- Add 15+ seconds of latency to scaling decisions +- Impact overall scaling performance + +See [Issue #4710](https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710) for details. + +## Solution Architecture + +``` +┌─────────────────┐ ┌─────────────────┐ +│ EC2 Instance │ State Change │ EventBridge │ +│ Lifecycle │ ─────────────────► │ Rule │ +└─────────────────┘ └────────┬────────┘ + │ + ▼ + ┌────────────────┐ + │ Counter Lambda │ + │ (update count) │ + └───────┬────────┘ + │ + ▼ +┌─────────────────┐ ┌───────────────────┐ +│ Scale-Up Lambda │ ◄──── Read ─────── │ DynamoDB Table │ +│ (check limit) │ │ ┌───────────────┐ │ +└─────────────────┘ │ │ pk: env#type │ │ + │ │ │ count: 42 │ │ + │ Fallback if stale │ │ updated: ts │ │ + ▼ │ └───────────────┘ │ +┌─────────────────┐ └───────────────────┘ +│ EC2 Describe │ +│ Instances │ +└─────────────────┘ +``` + +## Features + +- **Event-driven**: Uses EventBridge to react to EC2 state changes in real-time +- **Atomic counters**: DynamoDB atomic increments/decrements prevent race conditions +- **Auto-cleanup**: TTL on DynamoDB items prevents stale data accumulation +- **Fallback support**: Scale-up Lambda falls back to EC2 API if cache is stale +- **Low cost**: PAY_PER_REQUEST billing, typically pennies per month + +## Usage + +```hcl +module "runner_count_cache" { + source = "./modules/runner-count-cache" + + prefix = "github-runners" + environment_filter = "production" + + tags = { + Environment = "production" + } +} +``` + +## Integration with Scale-Up Lambda + +The scale-up Lambda can be configured to use this cache by setting these environment variables: + +- `RUNNER_COUNT_CACHE_TABLE_NAME`: DynamoDB table name +- `RUNNER_COUNT_CACHE_STALE_THRESHOLD_MS`: Maximum age of cached counts (default: 60000) + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.3.0 | +| aws | ~> 5.27 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| prefix | The prefix used for naming resources | `string` | n/a | yes | +| environment_filter | The environment tag value to filter EC2 instances | `string` | n/a | yes | +| tags | Map of tags to add to resources | `map(string)` | `{}` | no | +| kms_key_arn | Optional CMK Key ARN for DynamoDB encryption | `string` | `null` | no | +| cache_stale_threshold_ms | Max age before cache is considered stale | `number` | `60000` | no | +| ttl_seconds | TTL for DynamoDB items in seconds | `number` | `86400` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| dynamodb_table | DynamoDB table name and ARN | +| lambda_function | Counter Lambda function name and ARN | +| eventbridge_rule | EventBridge rule name and ARN | +| cache_config | Configuration for scale-up Lambda | diff --git a/modules/runner-count-cache/lambda.tf b/modules/runner-count-cache/lambda.tf new file mode 100644 index 0000000000..34d71ac9b7 --- /dev/null +++ b/modules/runner-count-cache/lambda.tf @@ -0,0 +1,149 @@ +# Counter Lambda Function +# Updates DynamoDB counter when EC2 instances change state + +data "aws_region" "current" {} +data "aws_caller_identity" "current" {} + +locals { + lambda_zip = "${path.module}/../../lambdas/functions/runner-count-cache/dist/runner-count-cache.zip" +} + +resource "aws_lambda_function" "counter" { + s3_bucket = var.lambda_s3_bucket != null ? var.lambda_s3_bucket : null + s3_key = var.counter_lambda_s3_key != null ? var.counter_lambda_s3_key : null + s3_object_version = var.counter_lambda_s3_object_version != null ? var.counter_lambda_s3_object_version : null + filename = var.lambda_s3_bucket == null ? local.lambda_zip : null + source_code_hash = var.lambda_s3_bucket == null && fileexists(local.lambda_zip) ? filebase64sha256(local.lambda_zip) : null + + function_name = "${var.prefix}-runner-count-cache" + role = aws_iam_role.counter.arn + handler = "index.handler" + runtime = var.lambda_runtime + timeout = var.counter_lambda_timeout + memory_size = var.counter_lambda_memory_size + architectures = [var.lambda_architecture] + tags = merge(local.tags, var.lambda_tags) + + environment { + variables = { + DYNAMODB_TABLE_NAME = aws_dynamodb_table.runner_counts.name + ENVIRONMENT_FILTER = var.environment_filter + TTL_SECONDS = var.ttl_seconds + LOG_LEVEL = "info" + POWERTOOLS_SERVICE_NAME = "runner-count-cache" + } + } + + dynamic "vpc_config" { + for_each = var.lambda_subnet_ids != null && var.lambda_security_group_ids != null ? [true] : [] + content { + security_group_ids = var.lambda_security_group_ids + subnet_ids = var.lambda_subnet_ids + } + } + + dynamic "tracing_config" { + for_each = var.tracing_config.mode != null ? [true] : [] + content { + mode = var.tracing_config.mode + } + } +} + +resource "aws_cloudwatch_log_group" "counter" { + name = "/aws/lambda/${aws_lambda_function.counter.function_name}" + retention_in_days = var.logging_retention_in_days + kms_key_id = var.logging_kms_key_id + tags = local.tags +} + +# IAM Role for Counter Lambda +resource "aws_iam_role" "counter" { + name = "${var.prefix}-runner-count-cache" + assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json + path = var.role_path + permissions_boundary = var.role_permissions_boundary + tags = local.tags +} + +data "aws_iam_policy_document" "lambda_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} + +# Policy for DynamoDB access +resource "aws_iam_role_policy" "counter_dynamodb" { + name = "dynamodb-access" + role = aws_iam_role.counter.id + policy = data.aws_iam_policy_document.counter_dynamodb.json +} + +data "aws_iam_policy_document" "counter_dynamodb" { + statement { + sid = "DynamoDBAccess" + actions = [ + "dynamodb:UpdateItem", + "dynamodb:GetItem", + "dynamodb:PutItem", + ] + resources = [aws_dynamodb_table.runner_counts.arn] + } +} + +# Policy for EC2 DescribeInstances (to get instance tags) +resource "aws_iam_role_policy" "counter_ec2" { + name = "ec2-describe" + role = aws_iam_role.counter.id + policy = data.aws_iam_policy_document.counter_ec2.json +} + +data "aws_iam_policy_document" "counter_ec2" { + statement { + sid = "EC2DescribeInstances" + actions = [ + "ec2:DescribeInstances", + "ec2:DescribeTags", + ] + resources = ["*"] + } +} + +# Policy for CloudWatch Logs +resource "aws_iam_role_policy" "counter_logs" { + name = "cloudwatch-logs" + role = aws_iam_role.counter.id + policy = data.aws_iam_policy_document.counter_logs.json +} + +data "aws_iam_policy_document" "counter_logs" { + statement { + sid = "CloudWatchLogs" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + resources = [ + "${aws_cloudwatch_log_group.counter.arn}:*", + ] + } +} + +# VPC policy if Lambda is in VPC +resource "aws_iam_role_policy_attachment" "counter_vpc" { + count = var.lambda_subnet_ids != null ? 1 : 0 + role = aws_iam_role.counter.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" +} + +# X-Ray tracing policy +resource "aws_iam_role_policy_attachment" "counter_xray" { + count = var.tracing_config.mode != null ? 1 : 0 + role = aws_iam_role.counter.name + policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" +} diff --git a/modules/runner-count-cache/main.tf b/modules/runner-count-cache/main.tf new file mode 100644 index 0000000000..e2f0147413 --- /dev/null +++ b/modules/runner-count-cache/main.tf @@ -0,0 +1,79 @@ +# Runner Count Cache Module +# +# This module creates a DynamoDB-based cache for tracking the number of active +# EC2 runners. It uses EventBridge to listen for EC2 state changes and updates +# a counter in DynamoDB, significantly reducing the need for DescribeInstances +# API calls during scale-up operations. +# +# This addresses the performance bottleneck described in Issue #4710: +# https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710 + +locals { + tags = var.tags +} + +# DynamoDB table to store runner counts per environment/type/owner +resource "aws_dynamodb_table" "runner_counts" { + name = "${var.prefix}-runner-counts" + billing_mode = "PAY_PER_REQUEST" # Auto-scales with no provisioning needed + + hash_key = "pk" # Format: "environment#runnerType#runnerOwner" + + attribute { + name = "pk" + type = "S" + } + + ttl { + attribute_name = "ttl" + enabled = true + } + + # Optional encryption with customer-managed KMS key + dynamic "server_side_encryption" { + for_each = var.kms_key_arn != null ? [1] : [] + content { + enabled = true + kms_key_arn = var.kms_key_arn + } + } + + point_in_time_recovery { + enabled = false # Not needed for cache data + } + + tags = merge(local.tags, { + Name = "${var.prefix}-runner-counts" + }) +} + +# EventBridge rule to capture EC2 instance state changes +resource "aws_cloudwatch_event_rule" "ec2_state_change" { + name = "${var.prefix}-runner-state-change" + description = "Captures EC2 instance state changes for GitHub Action runners" + + event_pattern = jsonencode({ + source = ["aws.ec2"] + detail-type = ["EC2 Instance State-change Notification"] + detail = { + state = ["running", "pending", "terminated", "stopped", "shutting-down"] + } + }) + + tags = local.tags +} + +# EventBridge target to invoke the counter Lambda +resource "aws_cloudwatch_event_target" "counter_lambda" { + rule = aws_cloudwatch_event_rule.ec2_state_change.name + arn = aws_lambda_function.counter.arn +} + +# Permission for EventBridge to invoke the Lambda +resource "aws_lambda_permission" "allow_eventbridge" { + statement_id = "AllowExecutionFromEventBridge" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.counter.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.ec2_state_change.arn +} diff --git a/modules/runner-count-cache/outputs.tf b/modules/runner-count-cache/outputs.tf new file mode 100644 index 0000000000..587fae2005 --- /dev/null +++ b/modules/runner-count-cache/outputs.tf @@ -0,0 +1,39 @@ +output "dynamodb_table" { + description = "DynamoDB table for runner counts" + value = { + name = aws_dynamodb_table.runner_counts.name + arn = aws_dynamodb_table.runner_counts.arn + } +} + +output "lambda_function" { + description = "Counter Lambda function" + value = { + name = aws_lambda_function.counter.function_name + arn = aws_lambda_function.counter.arn + } +} + +output "eventbridge_rule" { + description = "EventBridge rule for EC2 state changes" + value = { + name = aws_cloudwatch_event_rule.ec2_state_change.name + arn = aws_cloudwatch_event_rule.ec2_state_change.arn + } +} + +output "lambda_role" { + description = "IAM role for the counter Lambda" + value = { + name = aws_iam_role.counter.name + arn = aws_iam_role.counter.arn + } +} + +output "cache_config" { + description = "Configuration for scale-up Lambda to use the cache" + value = { + table_name = aws_dynamodb_table.runner_counts.name + stale_threshold_ms = var.cache_stale_threshold_ms + } +} diff --git a/modules/runner-count-cache/variables.tf b/modules/runner-count-cache/variables.tf new file mode 100644 index 0000000000..9d01659501 --- /dev/null +++ b/modules/runner-count-cache/variables.tf @@ -0,0 +1,127 @@ +variable "prefix" { + description = "The prefix used for naming resources" + type = string +} + +variable "tags" { + description = "Map of tags that will be added to created resources" + type = map(string) + default = {} +} + +variable "kms_key_arn" { + description = "Optional CMK Key ARN to be used for DynamoDB encryption. If not provided, AWS managed key will be used." + type = string + default = null +} + +variable "environment_filter" { + description = "The environment tag value to filter EC2 instances. Should match the 'ghr:environment' tag value." + type = string +} + +variable "counter_lambda_timeout" { + description = "Time out for the counter update lambda in seconds." + type = number + default = 30 +} + +variable "counter_lambda_memory_size" { + description = "Memory size limit in MB for counter update lambda." + type = number + default = 256 +} + +variable "lambda_runtime" { + description = "AWS Lambda runtime for the counter function." + type = string + default = "nodejs20.x" +} + +variable "lambda_architecture" { + description = "AWS Lambda architecture. Lambda functions using Graviton processors ('arm64') tend to have better price/performance." + type = string + default = "arm64" +} + +variable "lambda_s3_bucket" { + description = "S3 bucket from which to get the lambda function. When not set, the lambda will be built locally." + type = string + default = null +} + +variable "counter_lambda_s3_key" { + description = "S3 key for the counter lambda function." + type = string + default = null +} + +variable "counter_lambda_s3_object_version" { + description = "S3 object version for the counter lambda function." + type = string + default = null +} + +variable "logging_retention_in_days" { + description = "Specifies the number of days you want to retain log events." + type = number + default = 7 +} + +variable "logging_kms_key_id" { + description = "The KMS Key ARN to use for CloudWatch log group encryption." + type = string + default = null +} + +variable "tracing_config" { + description = "Configuration for lambda tracing." + type = object({ + mode = optional(string, null) + capture_http_requests = optional(bool, false) + capture_error = optional(bool, false) + }) + default = {} +} + +variable "lambda_subnet_ids" { + description = "List of subnets in which the lambda will be launched." + type = list(string) + default = null +} + +variable "lambda_security_group_ids" { + description = "List of security group IDs associated with the Lambda function." + type = list(string) + default = null +} + +variable "role_permissions_boundary" { + description = "Permissions boundary that will be added to the created role for the lambda." + type = string + default = null +} + +variable "role_path" { + description = "The path that will be added to the role." + type = string + default = null +} + +variable "lambda_tags" { + description = "Map of tags to add to the Lambda function." + type = map(string) + default = {} +} + +variable "ttl_seconds" { + description = "TTL for DynamoDB items in seconds. Items older than this will be automatically deleted." + type = number + default = 86400 # 24 hours +} + +variable "cache_stale_threshold_ms" { + description = "Maximum age in milliseconds before a cached count is considered stale and falls back to EC2 API." + type = number + default = 60000 # 60 seconds +} diff --git a/modules/runner-count-cache/versions.tf b/modules/runner-count-cache/versions.tf new file mode 100644 index 0000000000..42a40b33fd --- /dev/null +++ b/modules/runner-count-cache/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.3.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.21" + } + } +} diff --git a/modules/runners/scale-up.tf b/modules/runners/scale-up.tf index b97fefed4f..3fd96b5c11 100644 --- a/modules/runners/scale-up.tf +++ b/modules/runners/scale-up.tf @@ -60,6 +60,8 @@ resource "aws_lambda_function" "scale_up" { SUBNET_IDS = join(",", var.subnet_ids) ENABLE_ON_DEMAND_FAILOVER_FOR_ERRORS = jsonencode(var.enable_on_demand_failover_for_errors) JOB_RETRY_CONFIG = jsonencode(local.job_retry_config) + RUNNER_COUNT_CACHE_TABLE_NAME = var.runner_count_cache != null ? var.runner_count_cache.table_name : "" + RUNNER_COUNT_CACHE_STALE_THRESHOLD_MS = var.runner_count_cache != null ? var.runner_count_cache.stale_threshold_ms : 60000 } } @@ -170,3 +172,23 @@ resource "aws_iam_role_policy" "job_retry_sqs_publish" { kms_key_arn = var.kms_key_arn != null ? var.kms_key_arn : "" }) } + +resource "aws_iam_role_policy" "scale_up_runner_count_cache" { + count = var.runner_count_cache != null ? 1 : 0 + name = "runner-count-cache-policy" + role = aws_iam_role.scale_up.name + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowDynamoDBRead" + Effect = "Allow" + Action = [ + "dynamodb:GetItem" + ] + Resource = "arn:${var.aws_partition}:dynamodb:${var.aws_region}:${data.aws_caller_identity.current.account_id}:table/${var.runner_count_cache.table_name}" + } + ] + }) +} diff --git a/modules/runners/variables.tf b/modules/runners/variables.tf index 8c8a1a136c..db63647d7b 100644 --- a/modules/runners/variables.tf +++ b/modules/runners/variables.tf @@ -778,3 +778,18 @@ variable "lambda_event_source_mapping_maximum_batching_window_in_seconds" { error_message = "Maximum batching window must be between 0 and 300 seconds." } } + +variable "runner_count_cache" { + description = <<-EOF + Configuration for the runner count cache feature that reduces EC2 DescribeInstances API calls. + This is passed from the root module when the feature is enabled. + + `table_name`: DynamoDB table name for storing runner counts. + `stale_threshold_ms`: How long before a cached count is considered stale. + EOF + type = object({ + table_name = string + stale_threshold_ms = number + }) + default = null +} diff --git a/variables.runner-count-cache.tf b/variables.runner-count-cache.tf new file mode 100644 index 0000000000..8912f3e2c0 --- /dev/null +++ b/variables.runner-count-cache.tf @@ -0,0 +1,28 @@ +variable "runner_count_cache" { + description = <<-EOF + Configuration for the runner count cache feature. This feature reduces EC2 DescribeInstances API calls + during scale-up operations by maintaining an event-driven count of active runners in DynamoDB. + This addresses rate limiting issues in high-volume environments (20K+ runners/day). + + See: https://github.com/github-aws-runners/terraform-aws-github-runner/issues/4710 + + `enable`: Enable or disable the runner count cache feature. + `stale_threshold_ms`: How long (in milliseconds) before a cached count is considered stale and falls back to EC2 API. Default 60000 (1 minute). + `ttl_seconds`: TTL for DynamoDB items in seconds. Default 86400 (24 hours). + `lambda_memory_size`: Memory size limit in MB of the counter lambda. + `lambda_timeout`: Time out of the counter lambda in seconds. + `lambda_s3_key`: S3 key for lambda function. Required if using S3 bucket to specify lambdas. + `lambda_s3_object_version`: S3 object version for lambda function. + EOF + + type = object({ + enable = optional(bool, false) + stale_threshold_ms = optional(number, 60000) + ttl_seconds = optional(number, 86400) + lambda_memory_size = optional(number, 256) + lambda_timeout = optional(number, 30) + lambda_s3_key = optional(string, null) + lambda_s3_object_version = optional(string, null) + }) + default = {} +} From dba3dab50235a59d381a18aa89608ab9633ac8b9 Mon Sep 17 00:00:00 2001 From: s1v4-d <161426787+s1v4-d@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:26:28 +0000 Subject: [PATCH 2/2] fix: address Copilot review comments on runner count cache PR - Fix double-counting issue: only count 'running' state, ignore 'pending' - Add warning log when DynamoDB returns negative counts - Add EC2 permission comment explaining why resource='*' is required - Fix 'Time out' typo to 'Timeout' in variable descriptions - Fix README AWS provider version mismatch (~>5.27 -> >=6.21) - Add race condition documentation between cache layers - Add debug log when stale DynamoDB cache is used - Fix Terraform formatting Signed-off-by: s1v4-d <161426787+s1v4-d@users.noreply.github.com> --- .../control-plane/src/scale-runners/cache.ts | 10 ++++++++++ .../control-plane/src/scale-runners/scale-up.ts | 11 ++++++++++- lambdas/functions/runner-count-cache/src/lambda.ts | 9 ++++++--- modules/runner-count-cache/README.md | 2 +- modules/runner-count-cache/lambda.tf | 11 +++++++---- modules/runner-count-cache/outputs.tf | 4 ++-- modules/runner-count-cache/variables.tf | 2 +- modules/runners/scale-up.tf | 6 +++--- variables.runner-count-cache.tf | 2 +- 9 files changed, 41 insertions(+), 16 deletions(-) diff --git a/lambdas/functions/control-plane/src/scale-runners/cache.ts b/lambdas/functions/control-plane/src/scale-runners/cache.ts index a67d1bdb2a..48dc82d4ed 100644 --- a/lambdas/functions/control-plane/src/scale-runners/cache.ts +++ b/lambdas/functions/control-plane/src/scale-runners/cache.ts @@ -228,6 +228,16 @@ export class dynamoDbRunnerCountCache { logger.debug('DynamoDB cache hit', { pk, count, isStale, ageMs: Date.now() - updated }); + // Normalize negative counts to zero. This can happen due to race conditions with + // EventBridge events (e.g., termination event arrives before running event). + if (count < 0) { + logger.warn('DynamoDB cache returned negative count, normalizing to 0', { + pk, + rawCount: count, + updated, + }); + } + return { count: Math.max(0, count), isStale }; } catch (error) { logger.warn('Failed to read from DynamoDB cache', { pk, error }); 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 a64906f0a2..1f862369d2 100644 --- a/lambdas/functions/control-plane/src/scale-runners/scale-up.ts +++ b/lambdas/functions/control-plane/src/scale-runners/scale-up.ts @@ -394,6 +394,11 @@ export async function scaleUp(payloads: ActionRequestMessageSQS[]): Promise 0) { ec2RunnerCountCache.increment(environment, runnerType, group, instances.length); } diff --git a/lambdas/functions/runner-count-cache/src/lambda.ts b/lambdas/functions/runner-count-cache/src/lambda.ts index 64d8c12220..fb0085e070 100644 --- a/lambdas/functions/runner-count-cache/src/lambda.ts +++ b/lambdas/functions/runner-count-cache/src/lambda.ts @@ -153,16 +153,19 @@ export async function handler( // Generate partition key const pk = `${tags.environment}#${tags.type}#${tags.owner}`; - // Determine increment based on state + // Determine increment based on state. + // IMPORTANT: We only count 'running' state as +1 to avoid double-counting when instances + // transition from pending -> running. The 'pending' state is ignored because all instances + // that reach 'running' must first pass through 'pending', which would cause double-counting. let increment = 0; - if (state === 'running' || state === 'pending') { + if (state === 'running') { increment = 1; } else if (state === 'terminated' || state === 'stopped' || state === 'shutting-down') { increment = -1; } if (increment === 0) { - logger.debug('State does not affect counter', { state }); + logger.debug('State does not affect counter (pending or other transitional states are ignored)', { state }); return; } diff --git a/modules/runner-count-cache/README.md b/modules/runner-count-cache/README.md index 28a21c0905..96038cc981 100644 --- a/modules/runner-count-cache/README.md +++ b/modules/runner-count-cache/README.md @@ -75,7 +75,7 @@ The scale-up Lambda can be configured to use this cache by setting these environ | Name | Version | |------|---------| | terraform | >= 1.3.0 | -| aws | ~> 5.27 | +| aws | >= 6.21 | ## Inputs diff --git a/modules/runner-count-cache/lambda.tf b/modules/runner-count-cache/lambda.tf index 34d71ac9b7..dea524f4fa 100644 --- a/modules/runner-count-cache/lambda.tf +++ b/modules/runner-count-cache/lambda.tf @@ -26,10 +26,10 @@ resource "aws_lambda_function" "counter" { environment { variables = { - DYNAMODB_TABLE_NAME = aws_dynamodb_table.runner_counts.name - ENVIRONMENT_FILTER = var.environment_filter - TTL_SECONDS = var.ttl_seconds - LOG_LEVEL = "info" + DYNAMODB_TABLE_NAME = aws_dynamodb_table.runner_counts.name + ENVIRONMENT_FILTER = var.environment_filter + TTL_SECONDS = var.ttl_seconds + LOG_LEVEL = "info" POWERTOOLS_SERVICE_NAME = "runner-count-cache" } } @@ -110,6 +110,9 @@ data "aws_iam_policy_document" "counter_ec2" { "ec2:DescribeInstances", "ec2:DescribeTags", ] + # EC2 Describe* actions require resource = "*" - they cannot be scoped to specific + # instance ARNs. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-policy-structure.html + # The Lambda filters instances by tags after fetching to ensure only runner instances are counted. resources = ["*"] } } diff --git a/modules/runner-count-cache/outputs.tf b/modules/runner-count-cache/outputs.tf index 587fae2005..7f9bed01d9 100644 --- a/modules/runner-count-cache/outputs.tf +++ b/modules/runner-count-cache/outputs.tf @@ -33,7 +33,7 @@ output "lambda_role" { output "cache_config" { description = "Configuration for scale-up Lambda to use the cache" value = { - table_name = aws_dynamodb_table.runner_counts.name - stale_threshold_ms = var.cache_stale_threshold_ms + table_name = aws_dynamodb_table.runner_counts.name + stale_threshold_ms = var.cache_stale_threshold_ms } } diff --git a/modules/runner-count-cache/variables.tf b/modules/runner-count-cache/variables.tf index 9d01659501..fd5ccfff6a 100644 --- a/modules/runner-count-cache/variables.tf +++ b/modules/runner-count-cache/variables.tf @@ -21,7 +21,7 @@ variable "environment_filter" { } variable "counter_lambda_timeout" { - description = "Time out for the counter update lambda in seconds." + description = "Timeout for the counter update lambda in seconds." type = number default = 30 } diff --git a/modules/runners/scale-up.tf b/modules/runners/scale-up.tf index 3fd96b5c11..b3416a7fb1 100644 --- a/modules/runners/scale-up.tf +++ b/modules/runners/scale-up.tf @@ -182,9 +182,9 @@ resource "aws_iam_role_policy" "scale_up_runner_count_cache" { Version = "2012-10-17" Statement = [ { - Sid = "AllowDynamoDBRead" - Effect = "Allow" - Action = [ + Sid = "AllowDynamoDBRead" + Effect = "Allow" + Action = [ "dynamodb:GetItem" ] Resource = "arn:${var.aws_partition}:dynamodb:${var.aws_region}:${data.aws_caller_identity.current.account_id}:table/${var.runner_count_cache.table_name}" diff --git a/variables.runner-count-cache.tf b/variables.runner-count-cache.tf index 8912f3e2c0..09b3c4d63b 100644 --- a/variables.runner-count-cache.tf +++ b/variables.runner-count-cache.tf @@ -10,7 +10,7 @@ variable "runner_count_cache" { `stale_threshold_ms`: How long (in milliseconds) before a cached count is considered stale and falls back to EC2 API. Default 60000 (1 minute). `ttl_seconds`: TTL for DynamoDB items in seconds. Default 86400 (24 hours). `lambda_memory_size`: Memory size limit in MB of the counter lambda. - `lambda_timeout`: Time out of the counter lambda in seconds. + `lambda_timeout`: Timeout of the counter lambda in seconds. `lambda_s3_key`: S3 key for lambda function. Required if using S3 bucket to specify lambdas. `lambda_s3_object_version`: S3 object version for lambda function. EOF