Search code examples
amazon-web-servicessecurityaws-lambdaaws-secrets-managersam

Resolve secretsmanager when invoking sam template locally


I am trying to invoke a lambda locally with sam local invoke. The function invokes fine but my environment variables for my secrets are not resolving. The secrets resolve as expected when you deploy the function. But I want to avoid my local code and my deployed code being any different. So is there a way to resolve those secrets to the actual secret value at the time of invoking locally? Currently I am getting just the string value from the environment variable. Code below.

template.yaml

    # This is the SAM template that represents the architecture of your serverless application
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-basics.html

# The AWSTemplateFormatVersion identifies the capabilities of the template
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/format-version-structure.html
AWSTemplateFormatVersion: 2010-09-09
Description: >-
  onConnect

# Transform section specifies one or more macros that AWS CloudFormation uses to process your template
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-section-structure.html
Transform:
- AWS::Serverless-2016-10-31

# Resources declares the AWS resources that you want to include in the stack
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
Resources:
  # Each Lambda function is defined by properties:
  # https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction

  # This is a Lambda function config associated with the source code: hello-from-lambda.js
  helloFromLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/onConnect.onConnect
      Runtime: nodejs14.x
      MemorySize: 128
      Timeout: 100
      Environment:
        Variables:
          WSS_ENDPOINT: '{{resolve:secretsmanager:prod/wss/api:SecretString:endpoint}}'

onConnect.js

/**
 * A Lambda function that returns a static string
 */
exports.onConnect = async () => {
    const endpoint = process.env.WSS_ENDPOINT;
    console.log(endpoint);
    // If you change this message, you will need to change hello-from-lambda.test.js
    const message = 'Hellddfdsfo from Lambda!';

    // All log statements are written to CloudWatch
    console.info(`${message}`);
    
    return message;
}

Solution

  • I came up with a work around that will allow me to have one code base and "resolve" secrets/parameters locally.

    I created a very basic lambda layer who's only job is fetching secrets if the environment is set to LOCAL. import boto3

    def get_secret(env, type, secret):
        client = boto3.client('ssm')
        if env == 'LOCAL':
            if type == 'parameter':
                return client.get_parameter(
                    Name=secret,
                )['Parameter']['Value']
        else:
            return secret
    

    I set the environment with a parameter in the lambda that will be calling this layer. BTW this layer will resolve more than one secret eventually so that's why the nested if might look a little strange. This is how I set the environment:

    Resources:
      ...
      GetWSSToken:
        Type: AWS::Serverless::Function
        Properties:
          FunctionName: get_wss_token
          CodeUri: get_wss_token/
          Handler: app.lambda_handler
          Runtime: python3.7
          Timeout: 30
          Layers:
            - arn:aws:lambda:********:layer:SecretResolver:8
          Environment:
            Variables:
              ENVIRONMENT: !Ref Env
              JWT_SECRET: !FindInMap [ Map, !Ref Env, jwtsecret ]
         ...
    
    Mappings:
      Map:
        LOCAL:
          jwtsecret: jwt_secret
        PROD:
          jwtsecret: '{{resolve:ssm:jwt_secret}}'
        STAGING:
          jwtsecret: '{{resolve:ssm:jwt_secret}}'
    
    Parameters:
      ...
      Env:
        Type: String
        Description: Environment this lambda is being run in.
        Default: LOCAL
        AllowedValues:
          - LOCAL
          - PROD
          - STAGING
    

    Now I can simply call the get_secret method in my lambda and depending on what I set Env to the secret will either be fetched at runtime or returned from the environment variables.

    import json
    import jwt
    import os
    from datetime import datetime, timedelta
    from secret_resolver import get_secret
    
    def lambda_handler(event, context):
        secret = get_secret(os.environ['ENVIRONMENT'], 'parameter', os.environ['JWT_SECRET'])
        two_hours_from_now = datetime.now() + timedelta(hours=2)
        encoded_jwt = jwt.encode({"expire": two_hours_from_now.timestamp()}, secret, algorithm="HS256")
        return {
            "statusCode": 200,
            "body": json.dumps({
                "token": encoded_jwt
            }),
        }
    

    I hope this helps someone out there trying to figure this out. The main issue here is keeping the secrets out of the code base and be able to test locally with the same code that's going into production.