Friday, February 16, 2018

Inside a Lambda Runtime: A Peek into the Serverless Lair

Ever wondered what it is like inside a lambda? Stop wondering. Let's find out.

Ever since they surfaced in 2014, AWS's lambda functions have made themselves a steaming hot topic, opening up whole new annals in serverless computing. The stateless, zero-maintenance, pay-per-execution goodies are literally changing—if not uprooting— the very roots of the cloud computing paradigm. While other players like Google and MS Azure are entering the game, AWS is the clear winner so far.

Okay, preaching aside, what does it really look like inside a lambda function?

As per AWS folks, lambdas are driven by container technology; to be precise, AWS EC2 Container Service (ECS). Hence, at this point, a lambda is merely a Docker container with limited access from outside. However, the function code that we run inside the container has almost unlimited access to it—except root privileges— including the filesystem, built-in and installed commands and CLI tools, system metadata and stats, logs, and more. Not very useful for a regular lambda author, but could be so if you intend to go knee-deep in OS-level stuff.

Obviously, the easiest way to explore all these OS-level offerings is to have CLI (shell) access to the lambda environment. Unfortunately this is not possible at the moment; nevertheless, combining the insanely simple syntax provided by the NodeJS runtime and the fact that lambdas have a few minutes' keep-alive time, we can easily write a ten-liner lambda that can emulate a shell. Although a real "session" cannot be established in this manner (for example, you cannot run top for a real-time updating view), you can repeatedly run a series of commands as if you are interacting with a user console.

let {exec} = require('child_process');

exports.handle = (event, context, callback) => {
  console.log(event);
  exec(event.cmd, (err, stdout, stderr) => {
    console.log(stdout);
    if (err) console.log(stderr);
    callback(undefined, {statusCode: 200});
  });
}

Lucky for us, since the code is a mere ten-liner with zero external dependencies, we can deploy the whole lambda—including code, configurations and execution role—via a single CloudFormation template:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  shell:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: shell
      Handler: index.handle
      Runtime: nodejs6.10
      Code:
        ZipFile: >
          let {exec} = require('child_process');

          exports.handle = (event, context, callback) => {
            console.log(event);
            exec(event.cmd, (err, stdout, stderr) => {
              console.log(stdout);
              if (err) console.log(stderr);
              callback(undefined, {statusCode: 200});
            });
          }
      Timeout: 60
      Role:
        Fn::GetAtt:
        - role
        - Arn
  role:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: lambda.amazonaws.com

Deploying the whole thing is as easy as:

aws cloudformation deploy --stack-name shell --template-file /path/to/template.yaml --capabilities CAPABILITY_IAM

or selecting and uploading the template to the CloudFormation dashboard, in case you don't have the AWS CLI to do it the (above) nerdy way.

Once deployed, it's simply a matter of invoking the lambda with a payload containing the desired shell command:

{"cmd":"the command to be executed"}

If you have the AWS CLI, the whole thing becomes way more sexy, when invoked via the following shell snippet:

echo -n "> "
read cmd
while [ "$cmd" != "exit" ]; do
  echo
  aws lambda invoke --function-name shell --payload "{\"cmd\":\"$cmd\"}" --log-type Tail /tmp/shell.log --query LogResult --output text | base64 -d
  echo
  echo -n "> "
  read cmd
done

With this script in place, all you have is to invoke the script; you will be given a fake "shell" where you can execute your long-awaited command, and the lambda will execute it and return the output back to your console right away, dropping you back into the "shell" prompt:

> free

START RequestId: c143847d-12b8-11e8-bae7-1d25ba5302bd Version: $LATEST
2018-02-16T01:28:56.051Z c143847d-12b8-11e8-bae7-1d25ba5302bd { cmd: 'free' }
2018-02-16T01:28:56.057Z c143847d-12b8-11e8-bae7-1d25ba5302bd              total       used       free     shared    buffers     cached
Mem:       3855608     554604    3301004        200      44864     263008
-/+ buffers/cache:     246732    3608876
Swap:            0          0          0

END RequestId: c143847d-12b8-11e8-bae7-1d25ba5302bd
REPORT RequestId: c143847d-12b8-11e8-bae7-1d25ba5302bd Duration: 6.91 ms Billed Duration: 100 ms  Memory Size: 128 MB Max Memory Used: 82 MB

>

With this contraption you could learn quite a bit about the habitat and lifestyle of your lambda function. I, for starters, came to know that the container runtime environment comprises Amazon Linux instances, with around 4GB of (possibly shared) memoey and several (unusable) disk mounts of considerable size (in addition to the "recommended-for-use" 500MB mount on /tmp):

> df

START RequestId: bb0034fa-12ba-11e8-8390-cb81e1cfae92 Version: $LATEST
2018-02-16T01:43:04.559Z bb0034fa-12ba-11e8-8390-cb81e1cfae92 { cmd: 'df' }
2018-02-16T01:43:04.778Z bb0034fa-12ba-11e8-8390-cb81e1cfae92 Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/xvda1      30830568 3228824  27501496  11% /
/dev/loop8        538424     440    526148   1% /tmp
/dev/loop9           128     128         0 100% /var/task

END RequestId: bb0034fa-12ba-11e8-8390-cb81e1cfae92
REPORT RequestId: bb0034fa-12ba-11e8-8390-cb81e1cfae92 Duration: 235.44 ms Billed Duration: 300 ms  Memory Size: 128 MB Max Memory Used: 22 MB

> cat /etc/*-release

START RequestId: 6112efb9-12bd-11e8-9d14-d5c0177bc74f Version: $LATEST
2018-02-16T02:02:02.190Z 6112efb9-12bd-11e8-9d14-d5c0177bc74f { cmd: 'cat /etc/*-release' }
2018-02-16T02:02:02.400Z 6112efb9-12bd-11e8-9d14-d5c0177bc74f NAME="Amazon Linux AMI"
VERSION="2017.03"
ID="amzn"
ID_LIKE="rhel fedora"
VERSION_ID="2017.03"
PRETTY_NAME="Amazon Linux AMI 2017.03"
ANSI_COLOR="0;33"
CPE_NAME="cpe:/o:amazon:linux:2017.03:ga"
HOME_URL="http://aws.amazon.com/amazon-linux-ami/"
Amazon Linux AMI release 2017.03

END RequestId: 6112efb9-12bd-11e8-9d14-d5c0177bc74f
REPORT RequestId: 6112efb9-12bd-11e8-9d14-d5c0177bc74f Duration: 209.82 ms Billed Duration: 300 ms  Memory Size: 128 MB Max Memory Used: 22 MB

>

True, the output format (which is mostly raw from CloudWatch Logs) could be significantly improved, in addition to dozens of other possible enhancemenrs. So let's discuss, under comments!

No comments: