To contact other services in your Function, data structures such as SNS.Topic
, SQS.Queue
, DynamoDB.Table
, etc. are declared as a runtime Dependency.
First, create the Construct you want to access from Lambda, like an SQS.Queue
:
import { string } from '@punchcard/shape';
const queue = new SQS.Queue(stack, 'MyQueue', {
shape: string // data type will be orindary strings
});
Then, depend on it when creating a Lambda.Function
:
new Lambda.Function(stack, 'MyFunction', {
depends: queue,
// ..
});
This will grant the required IAM policies for your Function's IAM Role, add any environment variables for details such as the Queue's ARN, and create a client for accessing the Construct
at runtime.
The result is that your handle
function is now passed a queue
client instance which you can interact with to send messages.
new Lambda.Function(stack, 'MyFunction', {
depends: queue,
}, async (event, queue) => {
await queue.sendMessage('Hello, World!');
});
By depending on the Queue, we granted the Function's IAM Role read and write access (sqs:SendMessage
and sqs:ReceiveMessage
) to the Queue, when we really only needed permission to send messages. To narrow permissions, depend on the specific level of permissions you need:
new Lambda.Function(stack, 'MyFunction', {
depends: queue.sendAccess(),
// ..
});
This Function's IAM role now only has permission to sqs:SendMessage
.
A Lambda.Function
often requires access to multiple resources, not just one. You can compose multiple Dependencies into one type with the tuple
and named
combinators:
Dependency.tuple
- a tuple of Dependencies.
new Lambda.Function(stack, 'MyFunction', {
depends: Dependency.tuple(queue.sendAccess(), topic),
}, async (event, [queue, topic]) => {
await queue.sendMessage('Hello, SQS!');
await topic.publish('Hello, SNS!');
});
Dependency.named
- a set of explicitly named Dependencies (key-value pairs)
new Lambda.Function(stack, 'MyFunction', {
depends: Dependency.named({
queue: queue.sendAccess(),
topic
}),
}, async (event, ({queue, topic})) => {
await queue.sendMessage('Hello, SQS!');
await topic.publish('Hello, SNS!');
});
A Function's Runtime Dependencies are encoded in the type signature.
Ordinarily, an in-memory Function's signature would be Function<T, U>
(read: "a function from T
to U
"). In Punchcard, there's a third argument ,D
, which captures the Function's runtime Dependencies, Function<T, U, D extends Dependency<any>
(read: "a function from T
to U
with runtime Dependency D
").
In our example, the Queue has type:
SQS.Queue<StringShape>
Which implements "a Dependency
on an SQS.Client
for sending and receiving StringShape
messages" :
Dependency<SQS.Client<StringShape>>;
Resulting in this Function signature:
Lambda.Function<
CloudWatch.Event, // T - the cloud watch trigger event
any, // U - the return type doesn't matter
SQS.Client<StringShape> // D - runtime dependency on a Queue with String messages.
>;
The type signatures of composite dependencies read the same as how they're constructed:
const tupleDependency: Dependency.Tuple<[
SQS.ReadClient<StringShape>,
SNS.Client<StringShape>,
]> = Dependency.tuple(
queue.readAccess(),
topic
);
const namedDependency: Dependency.Named<{
queue: SQS.ReadClient<StringShape>,
topic: SNS.Client<StringShape>,
}> = Dependency.named({
queue: queue.readAccess(),
topic
}):
You may have noticed something strange about the definition of our SQS.Queue
:
const queue: SQS.Queue<StringShape> = new SQS.Queue(this, 'Q', {
shape: string
});
Why is it a SQS.Queue<StringShape>
instead of SQS.Queue<string>
? We'll explain what StringShape
is in the next section on Shapes.