Skip to content

feat(lambdas): add batch SSM parameter fetching to reduce API calls#5017

Open
thomasnemer wants to merge 2 commits intogithub-aws-runners:mainfrom
thomasnemer:batch-ssm-parameter-get
Open

feat(lambdas): add batch SSM parameter fetching to reduce API calls#5017
thomasnemer wants to merge 2 commits intogithub-aws-runners:mainfrom
thomasnemer:batch-ssm-parameter-get

Conversation

@thomasnemer
Copy link

@thomasnemer thomasnemer commented Feb 4, 2026

This PR intends to reduce SSM AWS API calls by doing the following:

Add getParameters() function to aws-ssm-util that fetches multiple SSM parameters in a single API call with automatic chunking (max 10 per call per AWS API limits).

Apply batch fetching to:

  • auth.ts: fetch App ID and Private Key in one call (2 calls → 1)
  • ConfigLoader.ts: fetch multiple matcher config paths in one call
  • ami.ts: batch resolve SSM parameter values for AMI lookups

Also remove redundant appId SSM fetch in scale-up.ts that was only used for logging.

Add getParameters() function to aws-ssm-util that fetches multiple SSM
parameters in a single API call with automatic chunking (max 10 per call
per AWS API limits).

Apply batch fetching to:
- auth.ts: fetch App ID and Private Key in one call (2 calls → 1)
- ConfigLoader.ts: fetch multiple matcher config paths in one call
- ami.ts: batch resolve SSM parameter values for AMI lookups

Also remove redundant appId SSM fetch in scale-up.ts that was only
used for logging.
@thomasnemer thomasnemer requested a review from a team as a code owner February 4, 2026 13:03
@Brend-Smits Brend-Smits requested a review from Copilot February 5, 2026 12:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR optimizes AWS SSM API usage by implementing batch parameter fetching to reduce the number of API calls. The changes introduce a new getParameters() function that retrieves multiple SSM parameters in a single API call (with automatic chunking for AWS's 10-parameter limit) and applies this optimization across multiple Lambda functions.

Changes:

  • Added getParameters() function to aws-ssm-util for batch SSM parameter fetching with automatic chunking
  • Replaced sequential SSM calls with batch fetching in authentication, configuration loading, and AMI housekeeping
  • Removed redundant appId SSM fetch in scale-up.ts that was only used for logging

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lambdas/libs/aws-ssm-util/src/index.ts Implements new getParameters() function for batch SSM parameter retrieval
lambdas/functions/webhook/src/ConfigLoader.ts Replaces sequential parameter fetching with batch call for matcher configs
lambdas/functions/webhook/src/ConfigLoader.test.ts Updates tests to mock getParameters() instead of individual getParameter() calls
lambdas/functions/control-plane/src/scale-runners/scale-up.ts Removes redundant appId SSM fetch used only for logging
lambdas/functions/control-plane/src/github/auth.ts Batches App ID and Private Key fetching into single SSM call
lambdas/functions/control-plane/src/github/auth.test.ts Updates tests to verify batch parameter fetching behavior
lambdas/functions/ami-housekeeper/src/ami.ts Refactors to use batch parameter fetching for AMI SSM lookups
lambdas/functions/ami-housekeeper/src/ami.test.ts Updates tests to use GetParametersCommand instead of GetParameterCommand

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

} else {
this.configLoadingErrors.push(
`Failed to load parameter for matcherConfig from path ${path}: Parameter not found`,
);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code continues processing remaining parameters even after encountering a missing parameter, which may lead to incomplete configuration. Consider returning early or throwing an error when a required parameter is not found to avoid partially constructed configurations.

Suggested change
);
);
// Stop further processing to avoid partially constructed matcherConfig
return;

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is already how the code was previously behaving. This can be challenged of course but would not really be the purpose of this PR imho.

// Handle wildcard patterns by first discovering matching parameters, then
// fetching their values
let wildcardValues: Promise<(string | undefined)[]> = Promise.resolve([]);
let wildcardValuesPromise: Promise<(string | undefined)[]> = Promise.resolve([]);
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name 'wildcardValuesPromise' is inconsistent with the original 'wildcardValues'. While the rename improves clarity, it creates inconsistency with the pattern used for 'explicitValuesPromise'.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion here.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

lambdas/functions/webhook/src/ConfigLoader.test.ts:203

  • There are no tests verifying the behavior when one or more parameters are missing from the getParameters() response. The code at lines 110-117 handles this by adding an error to configLoadingErrors, but this path is not tested. Add a test case where getParameters() returns a Map with only some of the requested parameters to verify that appropriate error messages are added for the missing ones.
    it('should load config successfully from multiple paths', async () => {
      process.env.PARAMETER_RUNNER_MATCHER_CONFIG_PATH = '/path/to/matcher/config-1:/path/to/matcher/config-2';
      process.env.PARAMETER_GITHUB_APP_WEBHOOK_SECRET = '/path/to/webhook/secret';

      const partialMatcher1 =
        '[{"id":"1","arn":"arn:aws:sqs:queue1","matcherConfig":{"labelMatchers":[["a"]],"exactMatch":true}}';
      const partialMatcher2 =
        ',{"id":"2","arn":"arn:aws:sqs:queue2","matcherConfig":{"labelMatchers":[["b"]],"exactMatch":true}}]';

      const combinedMatcherConfig = [
        { id: '1', arn: 'arn:aws:sqs:queue1', matcherConfig: { labelMatchers: [['a']], exactMatch: true } },
        { id: '2', arn: 'arn:aws:sqs:queue2', matcherConfig: { labelMatchers: [['b']], exactMatch: true } },
      ];

      // Mock getParameters for batch fetching multiple paths
      vi.mocked(getParameters).mockResolvedValue(
        new Map([
          ['/path/to/matcher/config-1', partialMatcher1],
          ['/path/to/matcher/config-2', partialMatcher2],
        ]),
      );

      vi.mocked(getParameter).mockImplementation(async (paramPath: string) => {
        if (paramPath === '/path/to/webhook/secret') return 'secret';
        return '';
      });

      const config: ConfigWebhook = await ConfigWebhook.load();

      expect(config.matcherConfig).toEqual(combinedMatcherConfig);
      expect(config.webhookSecret).toBe('secret');
    });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +37
const response = await ssmClient.send(
new GetParametersCommand({
Names: chunk,
WithDecryption: true,
}),
);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't handle errors from individual chunk API calls. If one GetParametersCommand fails, the error will propagate and prevent the function from fetching subsequent chunks. This means that if you request 15 parameters and the first chunk of 10 fails, you won't get the remaining 5 parameters even if they would have succeeded.

Consider implementing error handling per chunk to continue fetching other chunks even if one fails, similar to how the ami.ts implementation handles this (lines 226-232 in the diff).

Copilot uses AI. Check for mistakes.
);
expect(mockedGetParameters).not.toHaveBeenCalled();
});

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests covering the scenario where getParameters() succeeds but returns a Map that doesn't contain one or both of the requested parameters. The code checks for missing parameters at lines 87-92, but these error paths are not tested. Add tests to verify:

  1. Behavior when appIdParamName is not in the returned Map
  2. Behavior when appKeyParamName is not in the returned Map
  3. Behavior when both parameters are missing from the returned Map
Suggested change
it('Throws when appId parameter is missing from SSM result', async () => {
mockedGetParameters.mockResolvedValue(
new Map([[PARAMETER_GITHUB_APP_KEY_BASE64_NAME, b64]]),
);
await expect(createGithubAppAuth(installationId)).rejects.toThrow();
expect(mockedGetParameters).toHaveBeenCalledTimes(1);
});
it('Throws when appKey parameter is missing from SSM result', async () => {
mockedGetParameters.mockResolvedValue(
new Map([[PARAMETER_GITHUB_APP_ID_NAME, GITHUB_APP_ID]]),
);
await expect(createGithubAppAuth(installationId)).rejects.toThrow();
expect(mockedGetParameters).toHaveBeenCalledTimes(1);
});
it('Throws when both appId and appKey parameters are missing from SSM result', async () => {
mockedGetParameters.mockResolvedValue(new Map());
await expect(createGithubAppAuth(installationId)).rejects.toThrow();
expect(mockedGetParameters).toHaveBeenCalledTimes(1);
});

Copilot uses AI. Check for mistakes.
@@ -17,6 +17,35 @@ export async function getParameter(parameter_name: string): Promise<string> {
return result;
}

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function lacks JSDoc documentation. Given that this is a public API in a shared library (aws-ssm-util), it should have comprehensive documentation similar to the getParameter() function. The documentation should explain:

  • What the function does
  • The parameter types and their meaning
  • The return type and what the Map keys/values represent
  • What happens when parameters are not found (silently omitted from the Map)
  • The chunking behavior and AWS API limits
  • Any exceptions that might be thrown
Suggested change
/**
* Retrieves multiple parameters from AWS Systems Manager Parameter Store.
*
* This function uses the AWS SSM {@link GetParametersCommand} API to fetch the values
* for the provided parameter names. Requests are automatically chunked into batches
* of up to 10 names per call to comply with the AWS GetParameters API limit.
*
* Each successfully retrieved parameter is added to the returned {@link Map}, where:
* - The map key is the full parameter name as stored in Parameter Store.
* - The map value is the decrypted string value of the parameter.
*
* Parameter names that are not found in Parameter Store (or that cannot be returned
* by the API) are silently omitted from the resulting map. They will not appear as
* keys in the returned {@link Map}.
*
* @param parameter_names - An array of parameter names to retrieve from SSM Parameter Store.
* If the array is empty, an empty {@link Map} is returned without calling the AWS API.
*
* @returns A {@link Map} where each key is a parameter name and each value is the
* corresponding decrypted string value for that parameter. Only parameters that
* are successfully retrieved and have both a `Name` and a `Value` are included.
*
* @throws Error Propagates any error thrown by the underlying AWS SDK client,
* such as network errors, AWS service errors (e.g., access denied, throttling),
* or configuration issues (e.g., missing region or credentials).
*/

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +47
export async function getParameters(parameter_names: string[]): Promise<Map<string, string>> {
if (parameter_names.length === 0) {
return new Map();
}

const ssmClient = getTracedAWSV3Client(new SSMClient({ region: process.env.AWS_REGION }));
const result = new Map<string, string>();

// AWS SSM GetParameters API has a limit of 10 parameters per call
const chunkSize = 10;
for (let i = 0; i < parameter_names.length; i += chunkSize) {
const chunk = parameter_names.slice(i, i + chunkSize);
const response = await ssmClient.send(
new GetParametersCommand({
Names: chunk,
WithDecryption: true,
}),
);

for (const param of response.Parameters ?? []) {
if (param.Name && param.Value) {
result.set(param.Name, param.Value);
}
}
}

return result;
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new getParameters() function lacks test coverage. The vitest configuration requires 100% test coverage for all code in lambdas/libs/, but no tests were added for this new function. Tests should cover:

  • Happy path with multiple parameters
  • Empty parameter list
  • Chunking behavior when more than 10 parameters are provided
  • Error handling when SSM API calls fail
  • Handling of missing parameters (InvalidParameters in response)
  • Behavior when some parameters exist and others don't

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +44
for (const param of response.Parameters ?? []) {
if (param.Name && param.Value) {
result.set(param.Name, param.Value);
}
}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getParameters() function silently ignores parameters that don't have both Name and Value. If a parameter exists but has no value, it will be missing from the returned Map without any error or warning. This differs from the getParameter() function which explicitly throws an error when a parameter is not found (line 15). Consider either:

  1. Throwing an error if any requested parameters are not found in the response
  2. Checking the InvalidParameters field in the response to identify missing parameters
  3. At minimum, documenting this behavior in the function's JSDoc comment

This inconsistency could lead to subtle bugs where callers expect parameters to be present but receive an empty Map entry instead.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant