Plugins (beta)
⚠️ NOTE: Plugin support was added in version 8.6.0, is currently in beta, the interface is subject to change and only supports Node.js
Using @macros allows you to augment the Architect-generated CloudFormation before deployment. However, augmenting CloudFormation may not be sufficient for certain extensions. For example, if you want to extend Architect with:
- Your own custom Lambda integrations (e.g. Kinesis Stream or Lex conversation bot) and want Architect to
create,hydrateand be able to retrieve thelogsfor these functions - A better local development experience for your extension by hooking into the Architect
sandbox
Architect @plugins solves these use cases by providing a variety of interfaces that plugin authors may implement to hook into various Architect capabilities.
Architect Plugins are backwards-compatible with
@macros
Table of Contents
Installation
Similar to @macros, Architect will look for any @plugins in src/plugins or the project root node_modules. An app opts into using @plugins by adding them to app.arc:
@app
myapp
@plugins
myplugin
In the example above running any arc commands will look for ./src/plugins/myplugin, ./src/plugins/myplugin.js, ./node_modules/myplugin and finally ./node_modules/@myplugin. The myplugin entry in this example is assumed to be the plugin name.
Interface
Plugin authors should create a module that exports an object with properties of the object being different functions that hook into core Architect capabilities.
/**
* Starter plugin template
*/
module.exports = {
package: function ({ arc, cloudformation, stage='staging', inventory, createFunction }) {},
functions: function ({ arc, inventory }) {}, // also aliased to `pluginFunctions`
variables: function ({ arc, stage, inventory }) {},
sandbox: {
start: function ({ arc, inventory, invokeFunction, services }) {},
end: function ({ arc, inventory, services }) {}
}
}
A deep dive into the package, functions, variables, sandbox.start and sandbox.end methods follows.
package
package({ arc, cloudformation, stage, inventory, createFunction })
This method encapsulates Architect’s existing @macro functionality: extending Architect’s generated CloudFormation sam.json with your own custom extensions. The additional capability provided by @plugins over @macros is that @plugins provide a convenient way for your extension to define its own ephemeral cloud functions (AWS Lambdas). Plugin authors wanting to manage cloud functions in their plugins would:
- Leverage the convenience method
createFunction, which is injected as a parameter intopackage, to create CloudFormation JSON defining the AWS Lambda resources you want to manage within your plugin, and - Implement the
functionsplugin interface method to inform Architect of new Lambdas you are creating.
This method can be implemented as an async function or not.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
arc |
Object representing the parsed Architect project manifest file for the current project |
cloudformation |
The CloudFormation JSON template making up the Architect project |
stage |
The name of the environment; usually one of staging or production |
inventory |
An Architect inventory object representing the current Architect project |
createFunction |
A helper method for creating CloudFormation Resource JSON defining any cloud functions (AWS Lambdas) your plugin manages. Please see the createFunction section for details on this method. |
Returns
You must return the cloudformation argument after modifying it with your own extensions.
functions
functions({ arc, inventory })pluginFunctions({ arc, inventory })
The plugin author must implement this method if the plugin defines new Lambda functions. This method is used by Architect to allow your custom plugin Lambdas to hook into Architect’s capabilities and lifecycle in a variety of ways:
- instructing
arc createto create new files and directories in the project for your custom plugin Lambdas - instructing
arc hydrateto hydrate dependencies of your custom plugin Lambdas - instructing
arc logsas to where CloudWatch execution logs for your custom plugin Lambdas are located
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
arc |
Object representing the parsed Architect project manifest file for the current project |
inventory |
An Architect inventory object representing the current Architect project |
Returns
functions should return an array of objects, each object representing a new Lambda being defined by the plugin. Each object should have the following format:
{
src: '/Users/filmaj/src/my-arc-project/src/rules/rule-one',
body: 'exports.handler = async function (ruleEvent) { console.log(ruleEvent) }'
}
src: a string containing the fully qualified absolute path to the source code location for the Lambda function. This path must point to a location under the project’ssrc/directory. See the example section below on how to assemble such a path using the base project source directory available via theinventoryparameter.body: a string containing template code for the Lambda function handler
Example functions implementation
The following example implementation is for a plugin that allows consumers to define @rules Lambdas in their app.arc manifest:
let path = require('path')
module.exports = {
functions: function ({ arc, inventory }) {
if (!arc.rules) return [] // if plugin consumer didnt define any @rules, return empty array signifying no new lambdas to add
const cwd = inventory.inv._project.src // base project source directory
return arc.rules.map((rule) => { // for each @rules
let src = path.join(cwd, 'src', 'rules', rule[0]) // each @rules Lambda will exist under src/rules/<rule-name>
return {
src,
body: `exports.handler = async function (event) {
console.log(event);
};`
}
})
}
}
The above instructs Architect’s various capabilities to interact with cloud functions under the src/rules/<function> directory inside the project hierarchy. With the above functions method and given app.arc contents like so:
@rules
rule-one
rule-two
… running:
arc createwould create the folderssrc/rules/rule-oneandsrc/rules/rule-two, withindex.jsfiles in each containing the contents of thebodyreturned byfunctionsarc hydratewould hydrate the above two foldersarc logs src/rules/rule-onewould pull in any deployed-to-staging execution logs for therule-onefunction
variables
variables({ arc, cloudformation, stage, inventory })
The plugin author should implement this method if the plugin would like to provide any manner of data to Lambda functions at runtime. For example, perhaps you would like to expose the physical ID of some AWS resource (i.e. ARN) to your runtime code so that you can interact with it using the AWS SDK.
Architect provides a suite of runtime helpers via the @architect/functions library. This library leverages functionality provided by deploy and sandbox to expose runtime variables enabling service discovery - the automatic configuration, search and discovery of infrastructure and services making up your application. The variables plugin method enables plugin authors to hook into the Architect service discovery mechanism.
The variables plugin method is only necessary to implement if you would like your plugin to provide runtime data within Lambdas via the @architect/functions library. The exported variables would be available via the services function provided by @architect/functions (namespaced under the plugin name). For more information on how to query the service discovery mechanism using @architect/functions at runtime, check out the @architect/functions services documentation.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
arc |
Object representing the parsed Architect project manifest file for the current project |
cloudformation |
The CloudFormation JSON template making up the Architect project |
stage |
The name of the environment; usually one of testing, staging or production; testing is provided when running in a sandbox context whereas staging and production are provided at Architect CLI runtime when either arc deploy staging or arc deploy production are invoked, respectively |
inventory |
An Architect inventory object representing the current Architect project |
Returns
This method should always return an object. Each property on the object represents a variable name, and the value for each property contains the variable value.
🏌️♀️ Protip: When this method is invoked in a pre-deploy context, acceptable values for the variables include CloudFormation JSON. This is essential to expose CloudFormation-managed infrastructure; see the example below.
Example variables implementation
The following example variables implementation demonstrates a plugin that creates a new S3 Bucket. It may be desirable to provide variables related to the location of and credentials for the bucket:
module.exports = {
variables: function ({ arc, cloudformation, stage, inventory }) {
if (!arc['myS3Bucket']) return {} // if the user isn't using this plugin, return an empty object signifying no variables need exporting
const isLocal = stage === 'testing' // stage will equal 'testing' when running in sandbox, otherwise will be one of 'staging' or 'production' when running in a `deploy` context
const bucketName = `${arc.app}-newS3Bucket`
return {
bucketName,
accessKey: isLocal ? 'S3RVER' : { Ref: 'MyS3BucketCreds' },
secretKey: isLocal ? 'S3RVER' : { 'Fn::GetAtt': [ 'MyS3BucketCreds', 'SecretAccessKey' ] }
}
}
}
The above example returns three variables that would be provided at runtime: bucketName, accessKey and secretKey. Depending on whether the plugin executes in a local development environment context via sandbox or in a pre-deploy context via deploy, the contents of these credentials would differ:
- The
secretKeyandaccessKeyvariables would contain hard-coded values when running locally insandbox(both would have a value ofS3RVER). These hard-coded values could be used by the plugin author when implementing thesandbox.startmethod to provide a seamless local development experience. - The
secretKeyandaccessKeyvariables are CloudFormation JSON referencing a set of credentials calledMyS3BucketCredswhen running in a pre-deploy context. These dynamic values reference pre-existing CloudFormation Resources which would be implemented by the author in the plugin’spackagemethod.
The variables are namespaced on the @architect/functions services() object under a property matching the plugin name; check out the services documentation for more details.
Example service discovery usage with @architect/functions
How would a plugin consumer use these variables at runtime in their own application? Let’s take a look at the below example, which builds upon the S3 Bucket example from the previous section. It demonstrates one possible @http GET route implementation rendering a form allowing a user to upload to the plugin-generated S3 Bucket:
let arc = require('@architect/functions')
let form = require('./form') // helper that creates a form element we can render for users to upload their assets to our S3 bucket
let awsLite = require('@aws-lite/client')
exports.handler = arc.http(async function getIndex (req) {
const services = await arc.services()
const { bucketName, accessKey, secretKey } = services.imagebucket // plugin variables are namespaced under the plugin name; here we assume the plugin name is called 'imagebucket' and is present in the app's app.arc file as 'imagebucket' under the @plugins section
const region = process.env.AWS_REGION
const upload = form({ bucketName, accessKey, secretKey, region })
const aws = await awsLite()
const images = await aws.s3.ListObjects({ Bucket: bucketName, Prefix: 'thumb/' })
const imgTags = images.Contents.map(i => i.Key).map(i => `<img src="${i}" />`).join('\n')
return {
headers: {
'cache-control': 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0',
'content-type': 'text/html; charset=utf8'
},
body: `<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hi! Upload something directly from the browser to the S3 bucket:</h1>
${upload}
<h1>And here are all the previously uploaded images:</h1>
${imgTags}
</body>
</html>`
}
})
sandbox.start
start({ arc, inventory, invokeFunction, services }, callback)
The plugin author must implement this method if the plugin wants to hook into the startup routine for sandbox. This would allow plugin authors to emulate the cloud services their plugin provides in order to provide a local development experience for consumers of their plugin. It also allows modifying the behavior of sandbox’s built-in local development services for @http, @events, @queues and @tables via the services argument. Finally, a helper method invokeFunction (described in more detail below) is provided as an argument in order to allow plugin authors to invoke specific Lambdas from their plugin sandbox service code.
This method can either be async or not; if the plugin author implements it as async, then the final callback argument may be ignored. Otherwise, the callback argument should be invoked once the plugin’s sandbox service is ready.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
arc |
Object representing the parsed Architect project manifest file for the current project |
inventory |
An Architect inventory object representing the current Architect project |
invokeFunction |
A helper method that can be used for invoking any cloud functions (AWS Lambdas) your plugin manages during runtime in a local development context inside sandbox. Please see the invokeFunction section for details on this method. |
services |
An object containing http, events and tables properties that represent local servers that sandbox manages to provide a local development experience. A plugin author may want to modify the behavior of these pre-existing services in order for their plugin to provide a better local development experience. http is an instance of the npm package router and mocks API Gateway and Lambda. events is a Node.js HTTP server that mocks SNS and SQS by listening for JSON payloads and marshaling them to the relevant Lambda functions (see its listener module for more details). tables is an instance of the npm package dynalite and mocks DynamoDB. |
callback |
Can be ignored if the method implementation is an async function; otherwise, callback must be invoked once the plugin’s local development sandbox service is ready |
Example start implementation
An example is provided below that leverages the invokeFunction helper method.
sandbox.end
end({ arc, inventory, services }, callback)
If the plugin author implements the sandbox.start method, then they must also implement the sandbox.end method. This method gives the plugin the opportunity to gracefully shut down any services powering local development support of the plugin.
This method can either be async or not; if the plugin author implements it as async, then the final callback argument may be ignored. Otherwise, the callback argument should be invoked once the plugin’s sandbox service is ready.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
arc |
Object representing the parsed Architect project manifest file for the current project |
inventory |
An Architect inventory object representing the current Architect project |
services |
sandbox runs local in-memory servers to mock out HTTP, events, queues and database functionality; if you need to modify these services, use this argument |
callback |
Can be ignored if the method implementation is an async function; otherwise, callback must be invoked once the plugin’s local development sandbox service has been shut down |
Helper methods for plugin authors
For common Architect Plugin use cases, Architect provides a few helper functions available as parameters injected as arguments into plugin methods to make life easier for plugin authors.
createFunction
createFunction({ inventory, src })
This method should be leveraged inside a plugin’s package method in order to more easily define CloudFormation JSON representing Lambdas created by the plugin. Use of this method for defining Lambdas is an Architect best practice as certain specific conventions that Architect relies on can be maintained.
While the AWS Lambda logical ID is generally not a concern for developers using Architect, Architect relies on a logical ID naming convention to e.g. retrieve execution logs of a deployed Lambda via arc logs. This helper method helps enforce such conventions. Leveraging this method also gives the plugin-generated Lambdas transparent support for Architect’s per-function runtime configuration via the config.arc file.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
inventory |
An Architect inventory object representing the current Architect project |
src |
A string representing the fully qualified absolute path to where code for the Lambda exists locally |
Returns
A tuple (array of two objects) containing:
- A string representing an AWS-friendly Lambda resource name (which is based on the path to the function code), and
- A JSON object that can be assigned to a CloudFormation
sam.json’sResourcessection. This would define a Lambda that Architect would create during adeploy
Example usage of createFunction
let path = require('path')
module.exports = {
package: async function IoTRulesLambdas ({ arc, cloudformation, createFunction, stage = 'staging', inventory }) {
if (arc.rules) {
const cwd = inventory.inv._project.src
arc.rules.forEach(rule => {
let src = path.join(cwd, 'src', 'rules', rule[0])
let [functionName, functionDefn] = createFunction({ inventory, src })
cloudformation.Resources[functionName] = functionDefn
})
}
return cloudformation
}
}
invokeFunction
invokeFunction({ src, payload }, callback)
This method should be leveraged inside a plugin’s sandbox.start method in order to easily invoke project Lambdas locally within an npx arc sandbox local development runtime context. For example, if your plugin manages Lambdas related to some AWS service, it may be nice to provide a local development experience for consumers of your plugin. To provide a great local experience, consumers of your plugin will want to exercise your plugin-generated Lambdas when running locally. Using the combination of the sandbox.start and invokeFunction methods, plugin authors can implement a local development experience for plugin consumers.
Arguments
All arguments arrive as a bag of options with the following properties:
| Argument | Description |
|---|---|
src |
A string representing the fully qualified absolute path to where code for the Lambda exists locally |
payload |
JSON payload to deliver to the function |
callback |
Function with signature function(error, result) that is invoked with either the error or the result from the local function invocation |
Example usage of invokeLambda
The below plugin’s sandbox.start method listens for the “I” keyboard keypress, prompts the user which of the plugin’s Lambdas the user wants to invoke and what payload to deliver to the user, before using invokeFunction to invoke the Lambda code with the specified payload.
let path = require('path')
let prompt = require('prompt')
module.exports = {
functions: async function ({ arc, inventory }) {
if (!arc.rules) return []
const cwd = inventory.inv._project.src
return arc.rules.map((rule) => {
let src = path.join(cwd, 'src', 'rules', rule[0])
return {
src,
body: `exports.handler = async function (event) {
console.log(event)
}`
}
})
},
sandbox: {
start: function IoTRulesServiceStart ({ arc, inventory, invokeFunction, services }, callback) {
let rules = module.exports.functions({ arc, inventory }).map(rule => rule.src)
process.stdin.on('keypress', async function IoTRulesKeyListener (input, key) {
if (input === 'I') {
const response = await prompt([ {
type: 'select',
name: 'rule',
message: 'Which IoT Rule do you want to trigger an event for?',
choices: rules
}, {
type: 'input',
name: 'payload',
message: 'Type out the JSON payload you want to deliver to the rule (must be valid JSON!):',
initial: '{}',
validate: function (i) {
try {
JSON.parse(i)
}
catch (e) {
return e.message
}
return true
},
result: function (i) {
return JSON.parse(i)
}
} ])
invokeFunction({ src: response.rule, payload: response.payload }, function (err, result) {
if (err) console.error(`Error invoking lambda ${response.rule}!`, err)
else console.log(`${response.rule} invocation result:`, result)
})
}
})
console.log('IoT Rules Sandbox Service Started; press "I" (capital letter) to trigger a rule.')
callback()
}
}
}
Example plugins
- plugin-iot-rules: adds AWS IoT Topic event Lambdas
- plugin-parcel: compiles project Lambda code with the Parcel bundler both during local development via
sandboxand beforedeploys - arc-plugin-esbuild: compiles project Lambda code with the esbuild bundler watching during local development via
sandboxand beforedeploys - arc-plugin-s3-image-bucket: manages an S3 bucket purpose-built for allowing direct-from-user image uploads, includes support for customizable Lambda triggers based on bucket events