Rust: injecting configuration into your Lambda function with AWS AppConfig

Rust: injecting configuration into your Lambda function with AWS AppConfig

This blog post is all about AWS AppConfig and how we can use it to inject configuration at runtime.

As a part of the series Serverless Rust you can check out the other parts:

Part 1 and 2 describe how to set up Rust and VsCode.

Part 3 how to process in parallel AWS SQS messages.

Part 4 how to execute AWS Step Function for each AWS SQS message received.

AWS AppConfig

AWS AppConfig is part of AWS Systems Manager, and you can use it to create, manage, and deploy application configurations. In addition, it is compatible with AWS Lambda and many other services.

Based on the developer user guide AWS AppConfig offers the following benefits:

  • Reduce errors in configuration changes - it provides JSON schema and AWS Lambda function validation.
  • Deploy changes across a set of targets quickly - and has simple integration with AWS Lambda.
  • Update applications without interruptions - without taking your targets out of service.
  • Control deployment of changes across your application - you can enable deployment strategies

To configure AWS AppConfig, I should setup three types of resources:

  • Application
  • Environment
  • Configuration profile

This is an example of a CloudFormation Template to configure AppConfig:

// defines the “application”, the component where we group all our configurations 
MyApplication:
      Type: AWS::AppConfig::Application
      Properties:
        Name: "MyTestApplication"

//  defines a logical deployment group of AppConfig targets like test, stage, prod
  Environment:
    Type: AWS::AppConfig::Environment
    Properties:
      Name: "MyTestEnvironment"
      ApplicationId: { Ref: MyApplication }

  // defines the configuration, what in the end we will refer from the lambda to get the JSON 
  MyConfigurationProfile:
    Type: AWS::AppConfig::ConfigurationProfile
    Properties:
      ApplicationId: !Ref MyApplication
      Name: "MyTestProfile"
      LocationUri: "hosted"

 // A deployment strategy defines critical criteria for rolling out your configuration to the designated targets
  MyDeploymentStrategy:
    Type: AWS::AppConfig::DeploymentStrategy
    Properties:
      Name: "MyTestDeploymentStrategy"
      Description: "A deployment strategy to deploy the config immediately"
      DeploymentDurationInMinutes: 0
      FinalBakeTimeInMinutes: 0
      GrowthFactor: 100
      GrowthType: LINEAR
      ReplicateTo: NONE

// It is the “glue” that connects a configuration to an application JSON that defines the
// it is possible to avoid hardcoding values by using transformations and S3 for the uploading phase only.
  BasicHostedConfigurationVersion:
    Type: AWS::AppConfig::HostedConfigurationVersion
    Properties:
      ApplicationId: !Ref MyApplication
      ConfigurationProfileId: !Ref MyConfigurationProfile
      Description: 'A sample hosted configuration version'
      ContentType: 'application/json'
      Content: |
        { 
          "name": "ExampleApplication",
          "id": 1,
          "rank": 7
        }

// starts the deployment of the configuration
  AppConfigDeployment:
    Type: AWS::AppConfig::Deployment
    Properties:
      ApplicationId: !Ref MyApplication
      ConfigurationProfileId: !Ref MyConfigurationProfile
      ConfigurationVersion: !Ref BasicHostedConfigurationVersion
      DeploymentStrategyId: !Ref MyDeploymentStrategy
      EnvironmentId: !Ref Environment

In this example, I pass a configuration hardcoding its value in the template. It is not possible to load a JSON file from your local pipeline or filesystem. So if you decide to hardcode the configuration, remember there is a limitation of 4KB.

Suppose you want to use more than 4KB or do not like hardcode configuration into the template. In that case, you can upload it beforehand to AWS S3 and change the HostedConfigurationVersion section into:

BasicHostedConfigurationVersion: 
    Type: AWS::AppConfig::HostedConfigurationVersion 
    Properties: 
      ApplicationId: !Ref MyTestApplication 
      ConfigurationProfileId: !Ref ConfigurationProfileTest 
      Description: 'A sample hosted configuration version' 
      Fn::Transform: 
          Name: AWS::Include 
          Parameters: 
            Location: !Sub s3://${configBucketName}/${configurationName}.json 
      ContentType: application/json

AWS Lambda integration

appconfig.png

Thanks to AWS AppConfig Lambda extension as Lambda layer, the integration is simple, and with a few Environment variables to set the configuration is completed.

      Environment:
        Variables:
          APP_CONFIG_NAME: MyTestApplication
          AWS_APPCONFIG_ENVIRONMENT: MyTestEnvironment
          AWS_APPCONFIG_PROFILE: MyTestProfile
          AWS_APPCONFIG_EXTENSION_HTTP_PORT: 2772
      Layers:
        - arn:aws:lambda:eu-central-1:066940009817:layer:AWS-AppConfig-Extension:54

In concrete, to retrieve a specific configuration, you need to send a GET request from your Lambda to localhost, using the following URL convention:

async fn fetch_config(appconfig_profile: &str) -> Result<MyConfig, ApplicationError> {
    let appconfig_name = std::env::var("APP_CONFIG_NAME").expect("APP_CONFIG_NAME must be set");
    let appconfig_env = std::env::var("AWS_APPCONFIG_ENVIRONMENT").expect("AWS_APPCONFIG_ENVIRONMENT must be set");
    let appconfig_port = std::env::var("AWS_APPCONFIG_EXTENSION_HTTP_PORT").expect("AWS_APPCONFIG_EXTENSION_HTTP_PORT must be set");

    let url = format!("http://localhost:{}/applications/{}/environments/{}/configurations/{}", appconfig_port, appconfig_name, appconfig_env, appconfig_profile);
    log::info!("URL {:?}", url);
    let response = reqwest::get(url)
        .await?
        .json::<MyConfig>()
        .await?;

    Ok(response)
}

The complete example is available on GitHub.

AWS AppConfig - extras

  1. By default, you have 1000 TPS, and in a serverless world, it is easy to reach it. You can request an increase of quota opening a ticket.
  2. After the first request, the profile is cached, and so from now on, you hit the internal cache of AppConfig. What I have noticed is that even if you have this in place when you reach the 1000 TPS, you will get an error like invalid JSON response body at localhost:2772/applications/MyTestApplicati..: Unexpected token R in JSON at position 0
  3. Plan for retries for when AppConfig returns A 504 Gateway Timeout Error.
  4. Use the Lambda function context to cache the configuration you already have retrieved to avoid calling AWS AppConfig (point 2).
  5. You can access AWS AppConfig configuration from another account and use it as a central point for an application composed of multiple teams.
  6. You can prefetch configuration data.

A note for AWS AppConfig pricing

AWS AppConfig is expensive if you have a lot of cold starts, but in a cluster setup, this problem is not so evident.

This is the AppConfig bill for December 2021

Screenshot 2022-01-12 at 13.40.02.png

Without going into detail, if I keep strictly on a B2B serverless application where cold starts are common, for example, my last seven days:

Screenshot 2022-01-12 at 13.43.06.png

The AWS AppConfig price is much higher ($60 from the billing report) than the other serverless services like Lambda functions, for example, which cost $6.

Conclusions

Many will tell you to use AWS S3 to inject configuration, but AWS AppConfig is a good service. Of course, it can be cheaper, and it would be nice to improve the AWS Console experience. Still, I can validate and deploy configuration changes from a central location without interruptions, which could come in handy.