@c6fc/spellcraft-aws-lambda
v2.0.0
Published
Lambda function factory covering packaging, environment wiring and the IAM that goes with it.
Readme
SpellCraft AWS Lambda Module
Allows you to trivially add NodeJS Lambda functions to your SpellCraft spells via AWS Terraform.
Features
nodejs_function(name, region, options={}) renders a complete, working Lambda deployment from a
single call: the function itself, an execution role with inline and attached policies, a CloudWatch
log group, X-Ray tracing, an archive_file data source that zips your source directory, and a
null_resource that runs npm install in it first.
It's only reachable through config(), called once per file:
local lambda = (import "@c6fc/spellcraft-aws-lambda/module.libsonnet").config({ thisFile: std.thisFile });
lambda.nodejs_function("my_function", "us-east-1")Source for my_function is then expected at lambda_functions/my_function/, sibling to thisFile.
Why config() is required, not optional
An earlier version let nodejs_function() work standalone, defaulting thisFile to something that
resolved the same way ${path.module}/../lambda_functions/<name> always had. That default is
indistinguishable, from inside the function, from a plugin author who simply forgot to call
config() — and it's worse than just ambiguous: it can look correct during that author's own
standalone testing, because render/'s parent happens to be their plugin's own root when they're
the ones running it. It only actually breaks once someone else nests that plugin inside a different
project, far from wherever the mistake was made, with a confusing terraform apply-time "source
directory does not exist" rather than a clear failure at spellcraft generate time. Requiring
config() closes that off entirely: calling nodejs_function() without it fails immediately, by
name, for everyone, every time — plugin author or direct consumer alike.
thisFile can't have a default set here, either, for a related reason: std.thisFile is
lexical, not dynamic — it names whichever file the token is physically written in, regardless of
who calls the function that contains it or how deep the call chain goes. A default written in this
file would itself be lexically bound to this plugin's own path, not yours, so it has to come from
your own call site.
Shared defaults
Anything passed to config() besides thisFile becomes a default for every nodejs_function() call
made through the returned object — any option in the table below, or anything passed through to the
resource. A call's own options always wins over a config() default for the same key:
local lambda = (import "@c6fc/spellcraft-aws-lambda/module.libsonnet").config({
thisFile: std.thisFile,
tracing: "PassThrough",
});
lambda.nodejs_function("worker", "us-east-1") // inherits tracing: "PassThrough"
lambda.nodejs_function("scheduler", "us-east-1", { tracing: "Active" }) // overrides itNesting this plugin inside another
The same config() call is what lets this plugin work correctly when it's not the one being called
directly from a project's manifest:
// Inside @your-org/some-component/module.libsonnet
local lambda = (import "@c6fc/spellcraft-aws-lambda/module.libsonnet").config({ thisFile: std.thisFile });
{
build(region):: lambda.nodejs_function("worker", region),
// -> resolves lambda_functions/worker/ inside @your-org/some-component itself,
// not inside whatever project eventually installs it
}Optional keys — all hidden from the rendered resource, and all usable as config() defaults too:
| option | default | purpose |
|---|---|---|
| arns_allowed_to_invoke | [] | ARNs granted lambda:InvokeFunction |
| services_allowed_to_invoke | [] | Objects with principal and optionally source_arn |
| event_triggers | [] | aws_cloudwatch_event_rule bodies; rule, target and permission are wired up for you |
| execution_policy_attachments | [] | Managed policy ARNs to attach to the role |
| execution_policy_statements | [] | Inline IAM statement objects |
| cloudwatch_log_retention_days | 30 | Log group retention |
| retain_logs_on_destroy | true | Keep the log group when the function is destroyed |
| tracing | 'Active' | X-Ray mode: Active or PassThrough |
Any other key is passed through to the aws_lambda_function resource, so runtime, handler,
timeout, memory_size and environment can all be overridden.
refs — referencing what this call produces, without knowing its naming scheme
nodejs_function() builds more than one resource, each with its own derived name (lambda-my_function
for the role, for instance) — internal details a caller extending or wiring into them shouldn't have
to already know. The return value's hidden refs field makes every one of them addressable by the
same convention Terraform itself uses: "<resource_type>.<name>".
local fn = lambda.nodejs_function("my_function", "us-east-1", { timeout: 30 });
{
"lambda.tf.json": fn,
// Extend the role this call created, without knowing it's named
// "lambda-my_function" internally.
"extra.tf.json": {
resource: {
aws_iam_role_policy_attachment: {
extra: {
role: "${%s.id}" % fn.refs["aws_iam_role.lambda-my_function"].terraform_id,
policy_arn: "arn:aws:iam::aws:policy/SomeOtherPolicy",
},
},
},
},
}Each entry is the call's own info — name, region, and every option (as given, not merged with this
plugin's own built-in defaults) — plus one hidden field, terraform_id, holding the exact address to
build a reference from. fn.refs["aws_lambda_function.my_function"].region reads a raw value straight
back; "${%s.arn}" % fn.refs["..."].terraform_id builds a Terraform reference to whichever attribute
you need — refs never guesses which attribute you want, so it never goes stale as new ones matter.
refs is hidden (::), so — like config()'s internals — it never appears in the rendered .tf.json,
only in Jsonnet itself. The full set of keys for one call: aws_lambda_function.<name>,
aws_iam_role.lambda-<name>, aws_iam_role_policy.lambda-<name>,
aws_cloudwatch_log_group.lambda-<name>, local_file.lambda-<name>_envvars,
null_resource.npm_install-<name>, and data.archive_file.<name> (a data source's own reference
syntax already starts with data., so that's the type prefix there too).
Installation
Install the plugin as a dev dependency in your SpellCraft project:
npm install --save @c6fc/spellcraft-aws-lambdaOnce installed, import the module directly by package path:
local lambda = (import "@c6fc/spellcraft-aws-lambda/module.libsonnet").config({ thisFile: std.thisFile });
local aws = import "@c6fc/spellcraft-aws-terraform/module.libsonnet";
{
'providers.tf.json': {
provider: aws.providerAliases("us-east-1")
},
// Source lives in lambda_functions/my_function/, sibling to this file
'lambda.tf.json': lambda.nodejs_function("my_function", "us-east-1", {
timeout: 30,
memory_size: 512,
execution_policy_statements: [{
Effect: "Allow",
Action: ["s3:GetObject"],
Resource: "arn:aws:s3:::my-bucket/*"
}],
event_triggers: [{
schedule_expression: "rate(1 hour)"
}]
})
}This module renders Terraform only; pair it with
@c6fc/spellcraft-aws-terraform
for the AWS provider definitions and state backend.
Documentation
Regenerate the API and CLI sections above with npx spellcraft doc from this directory.
