Skip to content

Maintenance: replace EnvironmentVariablesService class with helper functions in Parameters #4138

Open
@dreamorosi

Description

@dreamorosi

Summary

In #3945 we added new zero-dependency functional utilities to read and parse environment variables. These new functions supersede the old class based environment service approach. We should refactor the Parameters package to use the new function based approach instead of the custom EnvironmentVariablesService class.

Why is this needed?

The rationale for the introduction of these utilities as per #3945 is as follows:

[M]ost of the advantage to customers will come in the form of smaller utilities over time, since we'll be able to use these helpers across other Powertools for AWS utilities and replace the existing EnvironmentVariablesService class-based model which is extremely verbose and requires props drilling if you want to access env variables deep into an inheritance stack.

This change will help us to eventually remove the EnvironmentVariablesService entirely from the code base. The Parameters package currently has its own extended version of the service that adds parameters-specific environment variable handling.

Which area does this relate to?

Parameters

Solution

Currently the Parameters package has its own EnvironmentVariablesService class that extends the common one:

import { EnvironmentVariablesService as CommonEnvironmentVariablesService } from '@aws-lambda-powertools/commons';

class EnvironmentVariablesService
  extends CommonEnvironmentVariablesService
  implements ConfigServiceInterface
{
  private parametersMaxAgeVariable = 'POWERTOOLS_PARAMETERS_MAX_AGE';
  private ssmDecryptVariable = 'POWERTOOLS_PARAMETERS_SSM_DECRYPT';

  public getParametersMaxAge(): number | undefined {
    const maxAge = this.get(this.parametersMaxAgeVariable);
    if (maxAge.length === 0) return undefined;
    const maxAgeAsNumber = Number.parseInt(maxAge, 10);
    if (Number.isNaN(maxAgeAsNumber)) {
      console.warn(`Invalid value for ${this.parametersMaxAgeVariable} environment variable: [${maxAge}], using default value of ${DEFAULT_MAX_AGE_SECS} seconds`);
    } else {
      return maxAgeAsNumber;
    }
  }

  public getSSMDecrypt(): string {
    return this.get(this.ssmDecryptVariable);
  }
}

This service is used in the BaseProvider and passed to GetOptions and GetMultipleOptions classes primarily for reading the POWERTOOLS_PARAMETERS_MAX_AGE environment variable.

Proposed refactor using new functional utilities (minimal changes approach):

Since the Parameters package has a different architecture with multiple provider classes extending BaseProvider, we'll take a minimal approach by replacing the service usage with direct functional utility calls:

import { getNumberFromEnv, getStringFromEnv } from '@aws-lambda-powertools/commons/utils/env';

// Replace EnvironmentVariablesService methods with utility functions
const getParametersMaxAge = (): number | undefined => {
  try {
    return getNumberFromEnv({
      key: 'POWERTOOLS_PARAMETERS_MAX_AGE',
    });
  } catch (error) {
    return undefined;
  }
};

const getSSMDecrypt = (): string => {
  return getStringFromEnv({
    key: 'POWERTOOLS_PARAMETERS_SSM_DECRYPT',
    defaultValue: '',
  });
};

Update BaseProvider constructor:

// Remove envVarsService property and initialization
abstract class BaseProvider implements BaseProviderInterface {
  // Remove: public envVarsService: EnvironmentVariablesService;
  protected client: unknown;
  protected store: Map<string, ExpirableValue>;

  public constructor({
    awsSdkV3Client,
    clientConfig,
    awsSdkV3ClientPrototype,
  }: BaseProviderConstructorOptions) {
    this.store = new Map();
    // Remove: this.envVarsService = new EnvironmentVariablesService();
    // ... rest of constructor
  }
}

Update GetOptions class:

import { getParametersMaxAge } from '../utils/env.js'; // New utility file

class GetOptions implements GetOptionsInterface {
  public constructor(
    // Remove: envVarsService: EnvironmentVariablesService,
    options: GetOptionsInterface = {}
  ) {
    Object.assign(this, options);

    if (options.maxAge === undefined) {
      this.maxAge = getParametersMaxAge() ?? DEFAULT_MAX_AGE_SECS;
    }
  }
}

Update GetMultipleOptions class:

class GetMultipleOptions extends GetOptions implements GetMultipleOptionsInterface {
  public constructor(
    // Remove: envVarsService: EnvironmentVariablesService,
    options: GetMultipleOptionsInterface = {}
  ) {
    super(options); // Remove envVarsService parameter
    // ... rest of constructor
  }
}

Update BaseProvider method calls:

// Update get method
protected async _get(
  name: string,
  options?: GetOptionsInterface
): Promise<unknown | undefined> {
  const configs = new GetOptions(options); // Remove envVarsService parameter
  // ... rest of method
}

// Update getMultiple method  
protected async _getMultiple(
  path: string,
  options?: GetMultipleOptionsInterface
): Promise<unknown> {
  const configs = new GetMultipleOptions(options); // Remove envVarsService parameter
  // ... rest of method
}

Update AppConfigProvider constructor:

``ts
import { getServiceName } from '@aws-lambda-powertools/commons/utils/env';

class AppConfigProvider extends BaseProvider {
public constructor(options: AppConfigProviderOptions) {
// ... rest of constructor
this.application = application ?? this.envVarsService.getServiceName();
// ... rest of constructor
}
}
``

Files to be modified:

  1. Remove packages/parameters/src/config/EnvironmentVariablesService.ts entirely
  2. Create packages/parameters/src/utils/env.ts with utility functions
  3. Update packages/parameters/src/base/BaseProvider.ts to remove envVarsService
  4. Update packages/parameters/src/base/GetOptions.ts to use utility functions
  5. Update packages/parameters/src/base/GetMultipleOptions.ts to use utility functions
  6. Update packages/parameters/src/appconfig/AppConfigProvider.ts to use env utilities

After the refactor, make sure the unit tests are still passing - update the tests as needed.

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmedThe scope is clear, ready for implementationgood-first-issueSomething that is suitable for those who want to start contributinghelp-wantedWe would really appreciate some support from community for this oneparametersThis item relates to the Parameters Utility

    Type

    No type

    Projects

    Status

    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions