We're Hiring! • Newsletter • Forum • Meetups • Twitter
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.
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.
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 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.
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
Website • Slack • Newsletter • Forum • Meetups • Twitter • We're Hiring
Also please do join the Components channel on our public Serverless-Contrib Slack to continue the conversation.
- Getting Started
- Trying it out
- Current Limitations
- Concepts
- Creating Components
- Docs
- Examples
Note: Make sure you have Node.js 8+ and npm installed on your machine.
npm install --global serverless-components
- 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.
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.
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.
Right now the only supported region is us-east-1
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).
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.
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 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 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.
Output types are the description of the outputs your component returns. You supply those outputTypes
in the component's serverless.yml
file:
type: aws-lambda
outputTypes:
name:
type: string
arn:
type: string
Your provisioning logic, or the deploy
method of your index.js
file, should return an outputs
object that matches the outputTypes declared in your component's serverless.yml
file. This output can be referenced in serverless.yml
as inputs to other components.
For example, the above aws-lambda
component's deploy
method returns outputs that look like this:
index.js
const deploy = (inputs, context) => {
// lambda provisioning logic
const res = doLambdaDeploy()
// return outputs
return {
arn: res.FunctionArn,
name: res.FunctionName
}
}
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 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
}
The framework supports variables from the following sources:
- Environment Variables: for example,
${env.GITHUB_TOKEN}
- Output: for example:
${myEndpoint.url}
, wheremyEndpoint
is the component alias as defined inserverless.yml
, andurl
is a property in the outputs object that is returned from themyEndpoint
provisioning function. - Self: for example,
${self.path}/frontend
, whereself.path
evaluates to the absolute path of the component's root folder.
The framework supports two types of environment variables:
- .env File: Create a .env file in the root directory of your project. Add environment-specific variables on new lines in the form of NAME=VALUE. For example:
SOME_ENV=foo
- CLI Running the command like this:
SOME_ENV=foo components deploy
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.
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
}
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.
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.
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
.
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.
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).
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!
To deploy your app, run
components deploy
To update an app at anytime, simply run deploy again
To get info about a deployed service, run
components info
To remove your app, run
components remove
Aside from using Serverless Components via the CLI you can also use the Framework programmatically.
Different commands are available via an exposed API.
You can use an existing Serverless Components project by providing the projectPath
option or you can define the structure
of your serverless.yml
file on the fly using the serverlessFileObject
option:
const path = require('path')
const { pkg, deploy, remove } = require('serverless-components')
const projectPath = path.join('my', 'project')
async function withProjectPath() {
console.log('Packaging service...')
await pkg({ projectPath, path: projectPath })
console.log('Deploying service...')
await deploy({ projectPath })
console.log('Re-deploying service...')
await deploy({ projectPath })
console.log('Removing service...')
await remove({ projectPath })
}
async function withServerlessFileObject() {
const serverlessFileObject = {
type: 'my-app',
version: '0.1.0',
components: {
myRole: {
type: 'tests-integration-iam-mock',
inputs: {
name: 'my-role-name',
service: 'my.function.service'
}
}
}
}
console.log('Deploying service...')
await deploy({ serverlessFileObject })
console.log('Re-deploying service...')
await deploy({ serverlessFileObject })
console.log('Removing service...')
await remove({ serverlessFileObject })
}
Promise.resolve()
.then(withProjectPath)
.then(withServerlessFileObject)
The deploy
API makes it possible to deploy a service.
await deploy(options)
Options:
projectPath
-string
- Path to the root of the project (defaults tocwd
)serverlessFileObject
-object
- Theserverless.yml
file representation as an object
The pkg
API makes it possible to package a project which creates a deployment artifact.
await pkg(options)
Options:
projectPath
-string
- Path to the root of the project (defaults tocwd
)serverlessFileObject
-object
- Theserverless.yml
file representation as an objectpath
-string
- Path to the project where theserverless.yml
file can be foundformat
-string
- The desired file format (zip
ortar
)
The remove
API makes it possible to remove a deployed service.
await remove(options)
Options:
projectPath
-string
- Path to the root of the project (defaults tocwd
)serverlessFileObject
-object
- Theserverless.yml
file representation as an object