AWS Lambda Aliases: A Practical Approach
Lambda functions are a fundamental component of the AWS serverless model. They provide a simple, cost-effective, and easily scalable programming model based on FaaS (functions as a service).
Lambda ARNs
Lambda functions can be referenced by their ARN (Amazon Resource Name). For example, the ARN to reference a 'helloworld' function in the 'us-east-2' region in account '3445435' would be:
arn:aws:lambda:us-east-2:3445435:function:helloworld
By default, referencing a Lambda function without any additional qualifiers will reference the code in the unpublished version of the Lambda, which is termed $LATEST. Therefore, these two ARNs are exactly equivalent:
arn:aws:lambda:us-east-2:3445435:function:helloworld
arn:aws:lambda:us-east-2:3445435:function:helloworld:$LATEST
Publishing and Referencing Lambda Versions
The act of "publishing" a Lambda function simply copies the code and the current values of its environment variables from $LATEST to a sequentially numbered version. Each time a Lambda is published the version number is incremented by 1.
A published version is basically an immutable snapshot of $LATEST which includes all of the following:
- The function code and all associated dependencies.
- The Lambda runtime that invokes the function.
- All the function settings, including the environment variables.
- A unique Amazon Resource Name (ARN) to identify the specific version of the function.
Any subsequent changes to the code and/or its environment variables can only be made to the unpublished (i.e., $LATEST) version. (As we shall see, this limitation has a significant downside with respect to the configuration in multiple environments.)
To reference a specific published version of a Lambda, we simply append the version number to the ARN. For example, to reference version 3 of our 'helloworld' function, we can use:
arn:aws:lambda:us-east-2:3445435:function:helloworld:3
Lambda Aliases
A Lambda Alias is simply a named pointer to a specific Lambda version. We can have multiple aliases for our Lambda function, each pointing to a different (or even the same) version.
Similar to versions, we can reference the aliased version of a Lambda function simply by appending the alias name to the ARN. Suppose we create an alias called test, and point it to version 3. Then the following ARNs are equivalent:
arn:aws:lambda:us-east-2:3445435:function:helloworld:test
arn:aws:lambda:us-east-2:3445435:function:helloworld:3
One of the advantages of using the alias (rather than the explicit version number), is the alias mapping can easily be changed to point to a different version, with no changes or impact to any consumers of the Lambda. Aliases can also facilitate blue/green traffic routing, canary deployments, etc. but for this article, we will focus on the configuration aspects.
Assume for example we have a test environment and a prod environment, and we wish to run different versions of the same Lambda in each. We can use aliases to point to different versions in each environment — the test alias might point to our newer bleeding-edge function version 3, while the prod alias might point to a more stable function version 2. At any time, we can simply re-point our aliases to different versions as needed to promote or roll back the versions for a specific environment.
Trouble in Paradise
Now let's assume our Lambda function needs to access a DynamoDB table. Rather than hard-coding the DynamoDB table name into the code, best practice would dictate that we externalize the configuration with a Lambda environment variable (e.g., TABLE_NAME), and reference that variable from within our code. Now, should we need to change the table name, we can simply change the value of the environment variable — no code changes are needed. If we're using Lambda aliases, we can simply publish a new version of the code, point the alias to the new version, and the config change will immediately take effect. Simple!
Well, not always. One of the advantages of aliases is the ability to more easily support different versions for multiple operating environments (like our previous test and prod example). In such a situation, it's actually quite likely we'd prefer using different DynamoDB tables for test vs. prod. (That is... unless we want to live dangerously and risk blowing away our production data!)
But as we know, a Lambda Alias is just a named pointer to some underlying immutable version. And that immutable version includes both the code and the values of its environment variables at the time it was published. So before we publish a new version of the code, we'll need to change the environment values each time to correspond to all of the multiple environments with different configurations.
For example, we might consider publishing 2 versions every time: one with the TABLE_NAME environment value set for the test, and another one for the TABLE_NAME environment value set for prod. Now imagine we have even more environments and dozens or hundreds of functions. Perhaps we could automate this somehow, but at best, it's error-prone and hard to manage. At worst, a mistake could prove disastrous.
A Practical Solution
So how can we leverage the advantages of Lambda Aliases while having a flexible configuration for multiple environments? We want our code to be immutable, but we need to inject different external configurations into that code, based on the environment our code is running in.
Rather than trying to bake in a fixed configuration at deployment time, we can instead use the value of the Lambda Alias at runtime to dynamically determine an appropriate location to load configuration data from. Depending on the use case, this could include AWS S3, AWS Systems Manager Parameter Store, a dedicated config server (e.g., Spring Cloud Config), etc. The general approach is applicable to any supported programming language supported by Lambda.
For simplicity, let's explore a straightforward Python code example using an S3 bucket to store Lambda configurations.
Python Example
Assume we want to support 3 Lambda function environments:
- A dev environment that simply points to our $LATEST Lambda version.
- A test environment pointed to a version referenced by a Lambda Alias called test.
- A prod environment pointed to a version referenced by a Lambda Alias called prod.
Let's create an S3 bucket called mycompany-lambda-config and store the following JSON files into it:
my-hello-world-LATEST.json:
{"customer-table-name":"my-customer-table-dev", "product-catalog-id":"DEV0001"}
my-hello-world-test.json:
{"customer-table-name":"customer-sample-testing", "product-catalog-id":"TEST1234"}
my-hello-world-prod.json:
{"customer-table-name":"customer-prod", "product-catalog-id":"CATALOG-2022"}
In our Lambda environment, we specify the S3 location using an environment variable with special placeholders for the function name and alias:
S3_CONFIG_SOURCE | s3://my-company-lambda-config/${functionName}-${aliasName}.json |
Now our objective is to determine, at runtime, the appropriate configuration to load from S3, based on the alias used to invoke the Lambda. The following Python code will accomplish this:
import boto3
import json
import os
# Replace ${functionName} and ${aliasName} placeholders in 'str' with values from current Lambda 'context'
# ARN format - arn:aws:lambda:aws-region:acct-id:function:function-name:alias-name
def replace_arn_placeholders(str, context):
arn = context.invoked_function_arn
parts = arn.split(':')
function_name = parts[6]
if len(parts) > 7:
alias_name = parts[7].replace('$LATEST', 'LATEST')
else:
alias_name = 'LATEST'
return str.replace('${functionName}', function_name).replace('${aliasName}', alias_name)
# Parse 's3_path' (e.g. 's3://bucket-name/path-part-1/path-part-2/...') and into bucket-name and full path components
def extract_bucket_and_key(s3_path):
path_parts = s3_path.replace("s3://","").split("/")
bucket = path_parts.pop(0)
key = "/".join(path_parts)
return bucket, key
# Read JSON config file located at 's3_config_source_with_placeholders' and return name/value pairs
def read_json_config_values(s3_config_source_with_placeholders, context):
s3_config_source = replace_arn_placeholders(s3_config_source_with_placeholders, context)
print('s3_config_source: ' + s3_config_source)
(s3_bucket, s3_key) = extract_bucket_and_key(s3_config_source)
s3 = boto3.client('s3')
obj = s3.get_object(Bucket=s3_bucket, Key=s3_key)
return json.loads(obj["Body"].read())
def lambda_handler(event, context):
print('invoked_function_arn: ' + context.invoked_function_arn)
print('function_version: ' + context.function_version)
s3_config_source_with_placeholders = os.environ.get('S3_CONFIG_SOURCE')
print('s3_config_source_with_placeholders: ' + s3_config_source_with_placeholders)
config_values = read_json_config_values(s3_config_source_with_placeholders, context)
message = '*** Currently configured values: customer-table-name = ' + config_values['customer-table-name'] + ', product-catalog-id = ' + config_values['product-catalog-id']
print(message)
return message
Results
(1) Called via arn:aws:lambda:aws-region:acct-id:function:my-hello-world
invoked_function_arn: arn:aws:lambda:us-east-1:512804910637:function:my-hello-world
function_version: $LATEST
s3_config_source_with_placeholders: s3://veriforge-lambda-config/${functionName}-${aliasName}.json
s3_config_source: s3://veriforge-lambda-config/my-hello-world-LATEST.json
*** Currently configured values:
customer-table-name = my-customer-table-dev, product-catalog-id = DEV0001
(2) Called via arn:aws:lambda:aws-region:acct-id:function:my-hello-world:test
invoked_function_arn: arn:aws:lambda:us-east-1:512804910637:function:my-hello-world:test
function_version: 12
s3_config_source_with_placeholders: s3://veriforge-lambda-config/${functionName}-${aliasName}.json
s3_config_source: s3://veriforge-lambda-config/my-hello-world-test.json
*** Currently configured values:
customer-table-name = customer-sample-testing, product-catalog-id = TEST1234
(3) Called via arn:aws:lambda:aws-region:acct-id:function:my-hello-world:prod
invoked_function_arn: arn:aws:lambda:us-east-1:512804910637:function:my-hello-world:prod
function_version: 11
s3_config_source_with_placeholders: s3://veriforge-lambda-config/${functionName}-${aliasName}.json
s3_config_source: s3://veriforge-lambda-config/my-hello-world-prod.json
*** Currently configured values:
customer-table-name = customer-prod, product-catalog-id = CATALOG-2022
Conclusion
We've demonstrated a simple technique for externalizing Lambda function configurations and dynamically loading the appropriate configuration at runtime based on the Lambda Alias the function was invoked with. The code required to implement this technique can easily be coded in any language runtime supported by Lambda. We simply need to:
- Define an environment variable pointing to a desired config source location (e.g. an S3 bucket).
- Parameterize the environment variable using dynamic placeholders for the function name and alias.
- Resolve the placeholders at runtime and load the appropriate configuration on-demand.