serverless-components

    0.2.0 • Public • Published

    serverless components logo

    Overview

    Easy

    A Serverless Component can package cloud/SaaS services, logic & automation into a simple building block you can use to build applications more easily than ever.

    Composable

    Serverless Components can be combined & nested. Reuse their functionality to build applications faster. Combine and nest them to make higher-order components, like features or entire applications.

    Open

    Serverless Components are 100% open-source & vendor-agnostic. You choose the services that best solve your problem, instead of being limited and locked into one platform.

    Serverless

    Serverless Components can deploy anything, but they're biased toward SaaS & cloud infrastructure with "serverless" qualities (auto-scaling, pay-per-use, zero-administration), so you can deliver apps with the lowest total cost & overhead.

    serverless components overview

    Example

    This example shows how an entire retail application can be assembled from components available. It provides the static frontend website, the REST API supporting the frontend and the database backing the REST API. Checkout the full example here.

    type: retail-app
     
    components:
      webFrontend:
        type: static-website
        inputs:
          name: retail-frontend
          contentPath: ${self.path}/frontend # define where to find the static files 
          # mustache templating is built in to the static-website component 
          templateValues:
            apiUrl: ${productsApi.url}
          contentIndex: index.html
     
      productsApi:
        type: rest-api
        inputs:
          gateway: aws-apigateway
          routes:
            /products: # routes begin with a slash 
              post: # HTTP method names are used to attach handlers 
                function: ${createProduct}
                cors: true
     
              # sub-routes can be declared hierarchically 
              /{id}: # path parameters use curly braces 
                get:
                  function: ${getProduct}
                  cors: true # CORS can be allowed with this flag 
     
            # multi-segment routes can be declared all at once 
            /catalog/{...categories}: # catch-all path parameters use ellipses 
              get:
                function: ${listProducts}
                cors: true
     
      createProduct:
        type: aws-lambda
        inputs:
          handler: products.create
          root: ${self.path}/code
          env:
            productTableName: products-${self.serviceId}
      getProduct:
        type: aws-lambda
        inputs:
          handler: products.get
          root: ${self.path}/code
          env:
            productTableName: products-${self.serviceId}
      listProducts:
        type: aws-lambda
        inputs:
          handler: products.list
          root: ${self.path}/code
          env:
            productTableName: products-${self.serviceId}
     
      productsDb:
        type: aws-dynamodb
        inputs:
          region: us-east-1
          tables:
            name: products-${self.serviceId}
              hashKey: id
              indexes:
                name: ProductIdIndex
                  type: global
                  hashKey: id
              schema:
                id: number
                name: string
                description: string
                price: number
              options:
                timestamps: true

    serverless Build Status npm version dependencies license

    WebsiteSlackNewsletterForumMeetupsTwitterWe're Hiring

    Also please do join the Components channel on our public Serverless-Contrib Slack to continue the conversation.

    Table of Contents

    Getting Started

    Note: Make sure you have Node.js 8+ and npm installed on your machine.

    1. npm install --global serverless-components
    2. Setup the environment variables
      • export AWS_ACCESS_KEY_ID=my_access_key_id
      • export AWS_SECRET_ACCESS_KEY=my_secret_access_key

    Run commands with:

    components [Command]
    

    Checkout the CLI docs for a list of all the available commands and instructions on how they work.

    Trying it out

    The best way to give components a try is to deploy one of the examples. We recommend checking out our retail-app example and to follow along with the instructions there.

    Current Limitations

    The following is a list with some limitations one should be aware of when using this project. NOTE: We're currently working on fixes for such issues and will announce them in our release notes / changelogs.

    us-east-1 only

    Right now the only supported region is us-east-1

    No rollback support

    Rolling back your application into the previous, stable state is currently not supported.

    However the framework ensures that your state file always reflects the correct state of your infrastructure setup (even if something goes wrong during deployment / removal).

    Concepts

    Components

    A component is the smallest unit of abstraction for your infrastructure. It can be a single small piece like an IAM role, or a larger piece that includes other small pieces, like github-webhook-receiver, which includes aws-lambda (which itself includes aws-iam-role), aws-apigateway (which also includes aws-iam-role), aws-dynamodb, and github-webhook. So components can be composed with each other in a component dependency graph to build larger components.

    You define a component using two files: serverless.yml for config, and index.js for the provisioning logic.

    The index.js file exports a deploy function and a remove function, both of which take two arguments: inputs and context. Each exported function name reflects the CLI command which will invoke it (the deploy function will be executed when one runs components deploy).

    These two files look something like this:

    serverless.yml

    type: my-component
     
    inputTypes: # type descriptions for inputs that my-component expects to receive 
      name:
        type: string
        required: true
      age:
        type: number
        required: false
        default: 47

    index.js

    const deploy = (inputs, context) => {
      // provisioning logic goes here
    }
     
    const remove = (inputs, context) => {
      // de-provisioning logic goes here
    }
     
    module.exports = {
      deploy,
      remove
    }

    However, this index.js file is optional, since your component can just be a composition of other smaller components without provisioning logic on its own.

    Composition

    Components can include other components in order to build up higher level use cases and expose a minimum amount of configuration.

    When composing components we simply include them in a components property within our own component's or application's serverless.yml file. In this example, my-component composes an aws-lambda and an aws-iam-role component.

    type: my-component
     
    components:
      myFunction:
        type: aws-lambda
        inputs:
          memory: 512
          timeout: 10
          handler: handler.handler
          role:
            arn: ${myRole.arn}
      myRole:
        type: aws-iam-role
        inputs:
          service: lambda.amazonaws.com

    Input types, Inputs & Outputs

    Input types

    Input types are the description of the inputs your component receives. You supply those inputTypes in the component's serverless.yml file:

    type: child-component
     
    inputTypes:
      name:
        type: string
        required: true
        default: John

    Or, if the component is being used as a child of another parent component, the parent will supply inputs and they can override the defaults that are defined at the child level:

    type: parent-component
     
    components:
      myChild:
        type: child-component
        inputs:
          name: Jane  # This overrides the default of "John" from the inputType 

    Inputs

    Inputs are the configuration that are supplied to your component's logic by the user. You define these inputs in the serverless.yml file where the component is being used:

    type: my-application
     
    components:
      myFunction:
        type: aws-lambda
        inputs:
          memory: 512 # This input sets the amount of memory the lambda function will use 
          timeout: 300 # This input sets the timeout for the aws-lambda function 

    Given this serverless.yml you would deploy a aws-lambda function with a memory size of 512 and timeout of 300.

    Outputs

    Your provisioning logic, or the deploy method of your index.js file, can optionally return an outputs object. This output can be referenced in serverless.yml as inputs to other components.

    For example, the lambda component's deploy method returns outputs that look like this:

    index.js

    const deploy = (inputs, context) => {
      // lambda provisioning logic
     
      // return outputs
      return {
        arn: res.FunctionArn
      }
    }
     
    module.exports = {
      deploy
    }

    These outputs can then be referenced by other components. In this example, we reference the function arn and pass it in to the aws-apigateway component to set up a handler for the route. Note that we use the component's alias myFunction to reference the arn output, i.e. ${myFunction.arn}

    type: my-application
     
    components:
      myFunction:
        type: aws-lambda
        inputs:
          handler: code.handler
      myEndpoint:
        type: aws-apigateway
        inputs:
          routes:
            /github/webhook:
              post:
                lambdaArn: ${myFunction.arn}

    State

    State can be accessed via the context object and represents a historical snapshot of what happened the last time you ran a command such as deploy, remove, etc.

    The provisioning logic can use this state object and compare it with the current inputs to make decisions around whether to run deploy, update or remove.

    The operation that will be fired depends on the inputs and how the provider works. Change in some inputs for some provider could trigger a create / remove while other inputs might trigger an update. It's up to the component to decide.

    Here's an example demonstrating how a lambda component decides what needs to be done based on the inputs and state objects:

    const deploy = async (inputs, context) => {
      let outputs;
      if (inputs.name && !context.state.name) {
        console.log(`Creating Lambda: ${inputs.name}`);
        outputs = await create(inputs);
      } else if (context.state.name && !inputs.name) {
        console.log(`Removing Lambda: ${context.state.name}`);
        outputs = await remove(context.state.name);
      } else if (inputs.name !== context.state.name) {
        console.log(`Removing Lambda: ${context.state.name}`);
        await remove(context.state.name);
        console.log(`Creating Lambda: ${inputs.name}`);
        outputs = await create(inputs);
      } else {
        console.log(`Updating Lambda: ${inputs.name}`);
        outputs = await update(inputs);
      }
      return outputs;
    }
     
    module.exports = {
      deploy
    }

    Variables

    The framework supports variables from the following sources:

    • Environment Variables: for example, ${env.GITHUB_TOKEN}
    • Output: for example: ${myEndpoint.url}, where myEndpoint is the component alias as defined in serverless.yml, and url is a property in the outputs object that is returned from the myEndpoint provisioning function.
    • Self: for example, ${self.path}/frontend, where self.path evaluates to the absolute path of the component's root folder.

    Graph

    Once you start composing components together with multiple levels of nesting, and all of these components depend on one another with variable references, you then end up with a graph of components.

    Internally, the framework constructs this dependency graph by analyzing the entire component structure and their variable references. With this dependency graph the framework is able to provision the required components in parallel whenever they either don't depend on each other, or are waiting on other components that haven't been provisioned yet.

    The component author / user doesn't have to worry about dependencies at all. They just use variables to reference the outputs as needed and it just works.

    Custom Commands

    Other than the built in deploy and remove commands, you can also include custom commands to add extra management capability for your component lifecycle. This is achieved by adding a corresponding function to the index.js file, just like the other functions in index.js.

    As usual, the test function receives inputs and context as parameters:

    const deploy = (inputs, context) => {
      // some provisioning logic
    }
     
    const test = (inputs, context) => {
      console.log('Testing the components functionality...')
    }
     
    module.exports = {
      deploy,
      test // make the function accessible from the CLI
    }

    Registry

    The Serverless Registry is a core part of the components implementation as it makes it possible to discover, publish and share existing components. For now, components registry ships with a number of built-in components that are usable by their type name.

    The registry is not only limited to serving components. Since components are functions, it's possible to wrap existing business logic into functions and publish them to the registry as well.

    Looking into the future, it will be even possible to serve functions which are written in different languages through the registry.

    Creating Components

    Here is a quick guide to help you kick-start your component development.

    Note: Make sure to re-visit the core concepts above, before you jump right into the component implementation.

    Basic setup

    In this guide we'll build a simple greeter component which will greet us with a custom message when we run the deploy, greet or remove commands.

    First, we need to create a dedicated directory for our component. This directory will include all the necessary files for our component, like its serverless.yml file, the index.js file (which includes the component's logic), and files such as package.json to define it's dependencies.

    Go ahead and create a greeter directory in the "Serverless Registry" directory located at registry.

    serverless.yml

    Let's start by describing our components interface. We define the interface in the serverless.yml file. Create this file in the components directory and paste in the following content:

    type: greeter
     
    inputTypes:
      firstName:
        type: string
        required: true
      lastName:
        type: string
        required: true

    Let's take a closer look at the code we've just pasted. At first, we define the type (think of it as an identifier or name) of the component. In our case the component is called greeter.

    Next up, we need to declare the inputTypes our component has. inputTypes define the shape of our inputs and are accessible from within the component's logic. In our case we expect a firstName and a lastName.

    That's it for the component definition. Let's move on to its implementation logic.

    index.js

    The component's logic is implemented with the help of an index.js file which is located in the root of the components directory. Go ahead and create an empty index.js file in the component's root directory.

    Then we'll implement the logic for the deploy, greet and remove commands. We do this by adding the respective functions into the file and exporting them so that the framework CLI can pick them up (Remember: only the exported functions are accessible via CLI commands).

    Just paste the following code in the index.js file:

    // "private" functions
    function greetWithFullName(inputs, context) {
      context.log(`Hello ${inputs.firstName} ${inputs.lastName}!`)
    }
     
    // "public" functions
    function deploy(inputs, context) {
      greetWithFullName(inputs, context)
     
      if (context.state.deployedAt) {
        context.log(`Last deployment: ${context.state.deployedAt}...`)
      }
     
      const deployedAt = new Date().toISOString()
     
      const updatedState = {
        ...context.state,
        ...inputs,
        deployedAt
      }
      context.saveState(updatedState)
     
      return updatedState
    }
     
    function greet(inputs, context) {
      context.log(`Hola ${inputs.firstName} ${inputs.lastName}!`)
    }
     
    function remove(inputs, context) {
      greetWithFullName(inputs, context)
      context.log('Removing...')
      context.saveState()
    }
     
    module.exports = {
      deploy,
      greet,
      remove
    }

    Let's take a closer look at the implementation.

    Right at the top we've defined a "helper" function we use to reduce code duplication (this function is not exported at the bottom and can therefore only be used internally). This greetWithFullName function gets inputs and context, and then logs a message which greets the user with his full name. Note that we're using the log function which is available at the context object instead of the native console.log function. The context object has other, very helpful functions and data attached to it.

    Next up, we've defined the deploy function. This function is executed every time the user runs a deploy command since we've exported it at the bottom of the file. At first, we re-use our greetWithFullName function to greet our user. Then we check the state to see if we've already deployed it. If this is the case we log out the timestamp of the last deployment. After that we get the current time and store it in an object which includes the state, the inputs and the new deployedAt timestamp. We store this object that reflects our current state. After that we return the object as outputs.

    The greet function is a custom command we use to extend the CLI's capabilities. Since we've exported it at the bottom of the file it'll be executed every time someone runs the greet command. The functionality is pretty straightforward. We just log out a different greeting using the context.log method and the inputs.

    The last function we've defined in our component's implementation is the remove function. The remove command is also accessible from the CLI because we export it at the bottom of the file. The function's code is also pretty easy to understand. At first we greet our user using the greetWithFullName helper function. Then we log a message that the removal was triggered and store an empty state (meaning that there's no more state information available).

    Testing

    Let's test our component!

    First of all let's create a new example application which uses our greeter component. cd into the examples directory by running:

    cd examples

    Create a new directory named test which has one serverless.yml file with the following content:

    type: my-application
     
    components:
      myGreeter:
        type: greeter
        inputs:
          firstName: John
          lastName: ${env.LAST_NAME}

    If we take a closer look at the serverless.yml file we can see that our lastName config value depends on an environment variable called LAST_NAME which is fetched from the local environment. This means that we need to export this variable so that the framework can pick it up and pass it down to our inputs:

    export LAST_NAME=Doe

    That's it. Let's take it for a spin. Run the following commands to test the components logic:

    components deploy
    
    components deploy
    
    components greet
    
    components remove
    

    Congratulations! You've successfully created your first Serverless component!

    Want to learn more? Make sure to take a look at all the different component implementations in the Serverless Registry!

    Docs

    CLI Usage

    deploy

    To deploy your app, run

    components deploy

    To update an app at anytime, simply run deploy again

    info

    To get info about a deployed service, run

    components info

    remove

    To remove your app, run

    components remove

    Component Docs

    Examples

    Keywords

    none

    Install

    npm i serverless-components

    DownloadsWeekly Downloads

    33

    Version

    0.2.0

    License

    Apache-2.0

    Unpacked Size

    1.88 MB

    Total Files

    306

    Last publish

    Collaborators

    • serverless-main