AWS Serverless Rust

AWS Serverless Rust

In June 2018 AWS Lambda added support for Rust language. Rust is a low-level programming language designed to be secure, small and fast. In opposite to languages like JavaScript, C# or Java it has no runtime environment. That is why I wanted to test it in a serverless project.

AWS Lambda functions are available for developers for some time but in the last two or three years, the serverless approach became popular around companies and production systems. One of the problems with Lambdas is a “cold start”. If function is idle, it shut down and next time, when it is needed it takes additional time to start before it executes your code. This startup time depends on the language you have selected to use.

Requirements

Today I’m going to show you a basic setup for Rust deployment to AWS. I need

  • AWS account
  • AWS CLI setup on your local machine
  • Serverless installed
  • Rust installed
  • Node.js

For my daily development, I’m using VS Code with few extensions (Rust, CodeLLDB for debugging, betterTOML and crates)..

Preparing project

Let’s start by creating a project directory and init node app. I need to install a serverless-rust plugin. It will be used for deployment.

mkdir rust-lambda
cd rust-lambda
npm init
npm install --save-dev serverless-rust

Now create the Cargo.toml file. In this file, you are going to list your Rust applications. Each application will be a separate API endpoint.

[workspace]
members = ["cognito-sync"]

The next step will be to create a new rust application and serverless.yaml file. I will be using rust CLI to create app, since I want to replace one function that helps me copying users form Cognito – let’s call it cognito-sync.

cargo new cognito-sync    

Now I should have a new directory with another Cargo file and mine source code. Before writing some code let’s quickly create a serverless config

service:
 name: rust-cognito-sync

plugins:
  - serverless-rust

provider:
  name: aws
  runtime: rust
  memorySize: 128
  stage: 'dev'
  region: 'eu-central-1' 
  timeout: 30
  logRetentionInDays: 3
  
package:
  individually: true

functions:
    cognito_sync:
        handler: cognito-sync
        events:
          - http:
                method: get
                path: /

If you are familiar with serverless there is nothing new here, except a serverless-rust plugin that will be used to compile and zip your functions. s.

Coding Rust for AWS Lambda

The Cargo file in rust is similar to package.json in node.js. I’m going to add some dependencies that will be used later in the code and name explicitly binary output. By doing this I’m sure that serverless will use this function.

[package]
name = "cognito-sync"
version = "0.1.0"
authors = ["Sufrago]
edition = "2018"
autobins = false

[dependencies]
http = "0.1.21"
lambda_http = "0.1.1"
lambda_runtime = "0.2.1"
log = "0.4.8"
serde = { version = "1.0.104", features = ["derive"] }
serde_derive = "1.0.104"
serde_json = "1.0.48"
simple_logger = "1.6.0"

[[bin]]
name = "cognito-sync"
path = "src/main.rs"

As you can see we have crates related to lambdas, serialization, and logging. That’s it for now.

Finally, I can write some actual code! “main.rs” is an entry to the app. I will keep it simple for now and just create a method to return API Gateway response with simple user JSON.

Let’s create a router function to execute the correct method.

fn router(req: Request, c: Context) -> Result<impl IntoResponse, HandlerError> {
    match req.method().as_str() {
        "GET" => get_user(req, c),
        _ => {
            let mut resp = Response::default();
            *resp.status_mut() = http::StatusCode::METHOD_NOT_ALLOWED;
            Ok(resp)
        }
    }
}

Since I want to call get_user, it will be a good moment to create the user and this method.

#[derive(Deserialize, Serialize, Debug, Clone)]
struct User {
    username: String,
    email: String,
}


fn get_user(_req: Request, _c: Context) -> Result<Response<Body>, HandlerError> {
    let user = User {
        username: "Marek".to_owned(),
        email: "test@test.com".to_owned(),
    };

    Ok(serde_json::json!(user).into_response())
}

For serialization, you can add derive annotations to user struct. Next, I just need to return my JSON using Lambda related structures provided in crates I have imported in cargo file. The code below will connect everything together.

fn main() -> Result<(), Box<dyn Error>> {
    simple_logger::init_with_level(log::Level::Info)?;
    lambda!(router);
    Ok(())
}

Serverless Testing and Deployment

Unfortunately, serverless-offline is not working yet with Rust so you need to execute serverless invoke for each function. The most important thing here is to provide the correct API gateway event. Just put this template in the payload.json file.

{
    "path": "/",
    "headers": {
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "Accept-Encoding": "gzip, deflate, lzma, sdch, br",
        "Accept-Language": "en-US,en;q=0.8",
        "CloudFront-Forwarded-Proto": "https",
        "CloudFront-Is-Desktop-Viewer": "true",
        "CloudFront-Is-Mobile-Viewer": "false",
        "CloudFront-Is-SmartTV-Viewer": "false",
        "CloudFront-Is-Tablet-Viewer": "false",
        "CloudFront-Viewer-Country": "US",
        "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
        "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)",
        "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==",
        "X-Forwarded-For": "192.168.100.1, 192.168.1.1",
        "X-Forwarded-Port": "443",
        "X-Forwarded-Proto": "https"
    },
    "pathParameters": {
        "proxy": "/"
    },
    "requestContext": {
        "accountId": "123456789012",
        "resourceId": "us4z18",
        "stage": "test",
        "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9",
        "identity": {
            "cognitoIdentityPoolId": "",
            "accountId": "",
            "cognitoIdentityId": "",
            "caller": "",
            "apiKey": "",
            "sourceIp": "192.168.100.1",
            "cognitoAuthenticationType": "",
            "cognitoAuthenticationProvider": "",
            "userArn": "",
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48",
            "user": ""
        },
        "resourcePath": "/{proxy+}",
        "httpMethod": "GET",
        "apiId": "wt6mne2s9k"
    },
    "resource": "/{proxy+}",
    "httpMethod": "GET",
    "queryStringParameters": {},
    "stageVariables": {
        "stageVarName": "stageVarValue"
    }
}

Now let’s test function by running serverless invoke like this:

serverless invoke local -f cognito_sync --path payload.json

Since it is building a docker image, it will take some time but the result should be something like this. As a side note, it is actually faster on my laptop to deploy it to AWS.

Serverless: Building native Rust cognito-sync func...
    Finished release [optimized] target(s) in 8.61s
objcopy: stVP1lFj: debuglink section already exists
  adding: bootstrap (deflated 61%)
Serverless: Packaging service...
Serverless: Building Docker image...
START RequestId: f306d4fe-df48-15a3-a4c1-0695234fd260 Version: $LATEST

2020-02-26 17:35:00,765 INFO  [lambda_runtime_core::runtime] Received new event with AWS request id: f306d4fe-df48-15a3-a4c1-0695234fd260

2020-02-26 17:35:00,807 INFO  [lambda_runtime_core::runtime] Response for f306d4fe-df48-15a3-a4c1-0695234fd260 accepted by Runtime API

END RequestId: f306d4fe-df48-15a3-a4c1-0695234fd260

REPORT RequestId: f306d4fe-df48-15a3-a4c1-0695234fd260  Init Duration: 425.31 ms        Duration: 137.88 ms    Billed Duration: 200 ms Memory Size: 1536 MB    Max Memory Used: 10 MB


{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{"content-type":["application/json"]},"body":"{\"email\":\"test@test.com\",\"username\":\"Marek\"}","isBase64Encoded":false}

Good, we have expected results with statusCode:200. Now let’s move it to AWS.

serverless deploy   

And you should get your API Endpoint link as a result:

endpoints:
  GET - https://XXXXXXXXX.execute-api.eu-central-1.amazonaws.com/dev/
functions:
  cognito_sync: rust-cognito-sync-dev-cognito_sync

That’s it. You have a fully working Lambda in Rust.

In the next post, I will try to connect to our Serverless RDS and do some database manipulation with Rust. This code is available on GitHub

Other articles from this series:

If you have an interesting project or need a highly qualified team, take a look at Sufrago.com to learn more about our company and get in touch.

Leave a Reply

Your email address will not be published. Required fields are marked *